diff --git a/src/Augmentation/Augmentation.tsx b/src/Augmentation/Augmentation.tsx index d64dedc90..eeb6d2cbf 100644 --- a/src/Augmentation/Augmentation.tsx +++ b/src/Augmentation/Augmentation.tsx @@ -55,7 +55,7 @@ interface IConstructorParams { } function generateStatsDescription(mults: IMap, programs?: string[], startingMoney?: number): JSX.Element { - const f = (x: number, decimals: number = 0) => { + const f = (x: number, decimals = 0) => { // look, I don't know how to make a "smart decimals" // todo, make it smarter if(x === 1.0777-1) return "7.77%"; diff --git a/src/Bladeburner.jsx b/src/Bladeburner.jsx deleted file mode 100644 index 2a3316fd1..000000000 --- a/src/Bladeburner.jsx +++ /dev/null @@ -1,3265 +0,0 @@ -import { Augmentations } from "./Augmentation/Augmentations"; -import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; -import { BitNodeMultipliers } from "./BitNode/BitNodeMultipliers"; -import { Engine } from "./engine"; -import { Faction } from "./Faction/Faction"; -import { Factions, factionExists } from "./Faction/Factions"; -import { joinFaction, displayFactionContent } from "./Faction/FactionHelpers"; -import { Player } from "./Player"; -import { hackWorldDaemon, redPillFlag } from "./RedPill"; -import { calculateHospitalizationCost } from "./Hospital/Hospital"; - -import { Page, routing } from "./ui/navigationTracking"; -import { numeralWrapper } from "./ui/numeralFormat"; - -import { dialogBoxCreate } from "../utils/DialogBox"; -import { - Reviver, - Generic_toJSON, - Generic_fromJSON, -} from "../utils/JSONReviver"; -import { setTimeoutRef } from "./utils/SetTimeoutRef"; -import { - formatNumber, - convertTimeMsToTimeElapsedString, -} from "../utils/StringHelperFunctions"; - -import { Settings } from "./Settings/Settings"; -import { ConsoleHelpText } from "./Bladeburner/data/Help"; -import { City } from "./Bladeburner/City"; -import { BladeburnerConstants } from "./Bladeburner/data/Constants"; -import { Skill } from "./Bladeburner/Skill"; -import { Skills } from "./Bladeburner/Skills"; -import { Operation } from "./Bladeburner/Operation"; -import { BlackOperation } from "./Bladeburner/BlackOperation"; -import { BlackOperations } from "./Bladeburner/BlackOperations"; -import { Contract } from "./Bladeburner/Contract"; -import { GeneralActions } from "./Bladeburner/GeneralActions"; -import { ActionTypes } from "./Bladeburner/data/ActionTypes"; - -import { addOffset } from "../utils/helpers/addOffset"; -import { clearObject } from "../utils/helpers/clearObject"; -import { createProgressBarText } from "../utils/helpers/createProgressBarText"; -import { exceptionAlert } from "../utils/helpers/exceptionAlert"; -import { getRandomInt } from "../utils/helpers/getRandomInt"; -import { getTimestamp } from "../utils/helpers/getTimestamp"; -import { KEY } from "../utils/helpers/keyCodes"; - -import { removeChildrenFromElement } from "../utils/uiHelpers/removeChildrenFromElement"; -import { appendLineBreaks } from "../utils/uiHelpers/appendLineBreaks"; -import { createElement } from "../utils/uiHelpers/createElement"; -import { createPopup } from "../utils/uiHelpers/createPopup"; -import { removeElement } from "../utils/uiHelpers/removeElement"; -import { removeElementById } from "../utils/uiHelpers/removeElementById"; - -import { StatsTable } from "./ui/React/StatsTable"; -import { CopyableText } from "./ui/React/CopyableText"; -import { Money } from "./ui/React/Money"; -import React from "react"; -import ReactDOM from "react-dom"; - -const stealthIcon = ` ` -const killIcon = `` - -// DOM related variables -const ActiveActionCssClass = "bladeburner-active-action"; - -// Console related stuff -let consoleHistoryIndex = 0; - -// Keypresses for Console -$(document).keydown(function(event) { - if (routing.isOn(Page.Bladeburner)) { - if (!(Player.bladeburner instanceof Bladeburner)) { return; } - let consoleHistory = Player.bladeburner.consoleHistory; - - if (event.keyCode === KEY.ENTER) { - event.preventDefault(); - var command = DomElems.consoleInput.value; - if (command.length > 0) { - Player.bladeburner.postToConsole("> " + command); - Player.bladeburner.resetConsoleInput(); - Player.bladeburner.executeConsoleCommands(command); - } - } - - if (event.keyCode === KEY.UPARROW) { - if (DomElems.consoleInput == null) {return;} - var i = consoleHistoryIndex; - var len = consoleHistory.length; - - if (len === 0) {return;} - if (i < 0 || i > len) { - consoleHistoryIndex = len; - } - - if (i !== 0) { - --consoleHistoryIndex; - } - - var prevCommand = consoleHistory[consoleHistoryIndex]; - DomElems.consoleInput.value = prevCommand; - setTimeoutRef(function(){DomElems.consoleInput.selectionStart = DomElems.consoleInput.selectionEnd = 10000; }, 0); - } - - if (event.keyCode === KEY.DOWNARROW) { - if (DomElems.consoleInput == null) {return;} - var i = consoleHistoryIndex; - var len = consoleHistory.length; - - if (len == 0) {return;} - if (i < 0 || i > len) { - consoleHistoryIndex = len; - } - - // Latest command, put nothing - if (i == len || i == len-1) { - consoleHistoryIndex = len; - DomElems.consoleInput.value = ""; - } else { - ++consoleHistoryIndex; - var prevCommand = consoleHistory[consoleHistoryIndex]; - DomElems.consoleInput.value = prevCommand; - } - } - } -}); - -function ActionIdentifier(params={}) { - if (params.name) {this.name = params.name;} - if (params.type) {this.type = params.type;} -} - -ActionIdentifier.prototype.toJSON = function() { - return Generic_toJSON("ActionIdentifier", this); -} - -ActionIdentifier.fromJSON = function(value) { - return Generic_fromJSON(ActionIdentifier, value.data); -} - -Reviver.constructors.ActionIdentifier = ActionIdentifier; - -function Bladeburner(params={}) { - this.numHosp = 0; // Number of hospitalizations - this.moneyLost = 0; // Money lost due to hospitalizations - this.rank = 0; - this.maxRank = 0; // Used to determine skill points - - this.skillPoints = 0; - this.totalSkillPoints = 0; - - this.teamSize = 0; // Number of team members - this.teamLost = 0; // Number of team members lost - - this.storedCycles = 0; - - this.randomEventCounter = getRandomInt(240, 600); // 4-10 minutes - - // These times are in seconds - this.actionTimeToComplete = 0; // 0 or -1 is an infinite running action (like training) - this.actionTimeCurrent = 0; - this.actionTimeOverflow = 0; - - // ActionIdentifier Object - var idleActionType = ActionTypes["Idle"]; - this.action = new ActionIdentifier({type:idleActionType}); - - this.cities = {}; - for (var i = 0; i < BladeburnerConstants.CityNames.length; ++i) { - this.cities[BladeburnerConstants.CityNames[i]] = new City({name: BladeburnerConstants.CityNames[i]}); - } - this.city = BladeburnerConstants.CityNames[2]; // Sector-12 - - // Map of SkillNames -> level - this.skills = {}; - this.skillMultipliers = {}; - this.updateSkillMultipliers(); // Calls resetSkillMultipliers() - - // Max Stamina is based on stats and Bladeburner-specific bonuses - this.staminaBonus = 0; // Gained from training - this.maxStamina = 0; - this.calculateMaxStamina(); - this.stamina = this.maxStamina; - - /** - * Contracts and Operations objects. These objects have unique - * properties because they are randomized in each instance and have stats like - * successes/failures, so they need to be saved/loaded by the game. - */ - this.contracts = {}; - this.operations = {}; - - // Object that contains name of all Black Operations that have been completed - this.blackops = {}; - - // Flags for whether these actions should be logged to console - this.logging = { - general:true, - contracts:true, - ops:true, - blackops:true, - events:true, - } - - // Simple automation values - this.automateEnabled = false; - this.automateActionHigh = 0; - this.automateThreshHigh = 0; //Stamina Threshold - this.automateActionLow = 0; - this.automateThreshLow = 0; //Stamina Threshold - - // Console command history - this.consoleHistory = []; - this.consoleLogs = []; - - // Initialization - this.initializeDomElementRefs(); - if (params.new) {this.create();} -} - -Bladeburner.prototype.prestige = function() { - this.resetAction(); - var bladeburnerFac = Factions["Bladeburners"]; - if (this.rank >= BladeburnerConstants.RankNeededForFaction) { - joinFaction(bladeburnerFac); - } -} - -Bladeburner.prototype.create = function() { - this.contracts["Tracking"] = new Contract({ - name:"Tracking", - desc:"Identify and locate Synthoids. This contract involves reconnaissance " + - "and information-gathering ONLY. Do NOT engage. Stealth is of the utmost importance.

" + - "Successfully completing Tracking contracts will slightly improve your Synthoid population estimate for " + - "whatever city you are currently in.", - baseDifficulty:125,difficultyFac:1.02,rewardFac:1.041, - rankGain:0.3, hpLoss:0.5, - count:getRandomInt(25, 150), countGrowth:getRandomInt(5, 75)/10, - 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", - desc:"Hunt down and capture fugitive Synthoids. These Synthoids are wanted alive.

" + - "Successfully completing a Bounty Hunter contract will lower the population in your " + - "current city, and will also increase its chaos level.", - baseDifficulty:250, difficultyFac:1.04,rewardFac:1.085, - rankGain:0.9, hpLoss:1, - count:getRandomInt(5, 150), countGrowth:getRandomInt(5, 75)/10, - 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", - desc:"Hunt down and retire (kill) rogue Synthoids.

" + - "Successfully completing a Retirement contract will lower the population in your current " + - "city, and will also increase its chaos level.", - baseDifficulty:200, difficultyFac:1.03, rewardFac:1.065, - rankGain:0.6, hpLoss:1, - count:getRandomInt(5, 150), countGrowth:getRandomInt(5, 75)/10, - 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", - desc:"As a field agent, investigate and identify Synthoid " + - "populations, movements, and operations.

Successful " + - "Investigation ops will increase the accuracy of your " + - "synthoid data.

" + - "You will NOT lose HP from failed Investigation ops.", - baseDifficulty:400, difficultyFac:1.03,rewardFac:1.07,reqdRank:25, - rankGain:2.2, rankLoss:0.2, - count:getRandomInt(1, 100), countGrowth:getRandomInt(10, 40)/10, - 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", - desc:"Conduct undercover operations to identify hidden " + - "and underground Synthoid communities and organizations.

" + - "Successful Undercover ops will increase the accuracy of your synthoid " + - "data.", - baseDifficulty:500, difficultyFac:1.04, rewardFac:1.09, reqdRank:100, - rankGain:4.4, rankLoss:0.4, hpLoss:2, - count:getRandomInt(1, 100), countGrowth:getRandomInt(10, 40)/10, - 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", - desc:"Conduct a sting operation to bait and capture particularly " + - "notorious Synthoid criminals.", - baseDifficulty:650, difficultyFac:1.04, rewardFac:1.095, reqdRank:500, - rankGain:5.5, rankLoss:0.5, hpLoss:2.5, - count:getRandomInt(1, 150), countGrowth:getRandomInt(3, 40)/10, - 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", - desc:"Lead an assault on a known Synthoid community. Note that " + - "there must be an existing Synthoid community in your current city " + - "in order for this Operation to be successful.", - baseDifficulty:800, difficultyFac:1.045, rewardFac:1.1, reqdRank:3000, - rankGain:55,rankLoss:2.5,hpLoss:50, - count:getRandomInt(1, 150), countGrowth:getRandomInt(2, 40)/10, - 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", - desc:"Lead a covert operation to retire Synthoids. The " + - "objective is to complete the task without " + - "drawing any attention. Stealth and discretion are key.", - baseDifficulty:1000, difficultyFac:1.05, rewardFac:1.11, reqdRank:20e3, - rankGain:22, rankLoss:2, hpLoss:10, - count:getRandomInt(1, 150), countGrowth:getRandomInt(1, 20)/10, - 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", - desc:"Assassinate Synthoids that have been identified as " + - "important, high-profile social and political leaders " + - "in the Synthoid communities.", - baseDifficulty:1500, difficultyFac:1.06, rewardFac:1.14, reqdRank:50e3, - rankGain:44, rankLoss:4, hpLoss:5, - count:getRandomInt(1, 150), countGrowth:getRandomInt(1, 20)/10, - 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, - }); -} - -Bladeburner.prototype.storeCycles = function(numCycles=1) { - this.storedCycles += numCycles; -} - - -Bladeburner.prototype.process = function() { - // Edge case condition...if Operation Daedalus is complete trigger the BitNode - if (redPillFlag === false && this.blackops.hasOwnProperty("Operation Daedalus")) { - return hackWorldDaemon(Player.bitNodeN); - } - - // If the Player starts doing some other actions, set action to idle and alert - if (Augmentations[AugmentationNames.BladesSimulacrum].owned === false && Player.isWorking) { - if (this.action.type !== ActionTypes["Idle"]) { - let msg = "Your Bladeburner action was cancelled because you started doing something else."; - if (this.automateEnabled) { - msg += `

Your 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) { - var 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 (let contract of Object.values(this.contracts)) { - contract.count += (seconds * contract.countGrowth/BladeburnerConstants.ActionCountGrowthPeriod); - } - for (let op of Object.values(this.operations)) { - op.count += (seconds * op.countGrowth/BladeburnerConstants.ActionCountGrowthPeriod); - } - - // Chaos goes down very slowly - for (let cityName of BladeburnerConstants.CityNames) { - var city = this.cities[cityName]; - if (!(city instanceof City)) {throw new Error("Invalid City object 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); - } - } - } - - if (routing.isOn(Page.Bladeburner)) { - this.updateContent(); - } - } -} - -Bladeburner.prototype.calculateMaxStamina = function() { - const effAgility = Player.agility * this.skillMultipliers.effAgi; - let 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)) {throw new Error("Max Stamina calculated to be NaN in Bladeburner.calculateMaxStamina()");} -} - -Bladeburner.prototype.calculateStaminaGainPerSecond = function() { - var effAgility = Player.agility * this.skillMultipliers.effAgi; - var maxStaminaBonus = this.maxStamina / BladeburnerConstants.MaxStaminaToGainFactor; - var gain = (BladeburnerConstants.StaminaGainPerSecond + maxStaminaBonus) * Math.pow(effAgility, 0.17); - return gain * (this.skillMultipliers.stamina * Player.bladeburner_stamina_gain_mult); -} - -Bladeburner.prototype.calculateStaminaPenalty = function() { - return Math.min(1, this.stamina / (0.5 * this.maxStamina)); -} - -Bladeburner.prototype.changeRank = function(change) { - 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); - - var bladeburnersFactionName = "Bladeburners"; - if (factionExists(bladeburnersFactionName)) { - var bladeburnerFac = Factions[bladeburnersFactionName]; - if (!(bladeburnerFac instanceof Faction)) { - throw new Error("Could not properly get Bladeburner Faction object in Bladeburner UI Overview Faction button"); - } - if (bladeburnerFac.isMember) { - var favorBonus = 1 + (bladeburnerFac.favor / 100); - bladeburnerFac.playerReputation += (BladeburnerConstants.RankToFactionRepFactor * change * Player.faction_rep_mult * favorBonus); - } - } - - // Gain skill points - var rankNeededForSp = (this.totalSkillPoints+1) * BladeburnerConstants.RanksPerSkillPoint; - if (this.maxRank >= rankNeededForSp) { - // Calculate how many skill points to gain - var gainedSkillPoints = Math.floor((this.maxRank - rankNeededForSp) / BladeburnerConstants.RanksPerSkillPoint + 1); - this.skillPoints += gainedSkillPoints; - this.totalSkillPoints += gainedSkillPoints; - } -} - -Bladeburner.prototype.getCurrentCity = function() { - var city = this.cities[this.city]; - if (!(city instanceof City)) { - throw new Error("Bladeburner.getCurrentCity() did not properly return a City object"); - } - return city; -} - -Bladeburner.prototype.resetSkillMultipliers = function() { - 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, - }; -} - -Bladeburner.prototype.updateSkillMultipliers = function() { - this.resetSkillMultipliers(); - for (var skillName in this.skills) { - if (this.skills.hasOwnProperty(skillName)) { - var skill = Skills[skillName]; - if (skill == null) { - throw new Error("Could not find Skill Object for: " + skillName); - } - var level = this.skills[skillName]; - if (level == null || level <= 0) {continue;} //Not upgraded - - var multiplierNames = Object.keys(this.skillMultipliers); - for (var i = 0; i < multiplierNames.length; ++i) { - var multiplierName = multiplierNames[i]; - if (skill[multiplierName] != null && !isNaN(skill[multiplierName])) { - var value = skill[multiplierName] * level; - var multiplierValue = 1 + (value / 100); - if (multiplierName === "actionTime") { - multiplierValue = 1 - (value / 100); - } - this.skillMultipliers[multiplierName] *= multiplierValue; - } - } - } - } -} - -Bladeburner.prototype.upgradeSkill = function(skill) { - // This does NOT handle deduction of skill points - var skillName = skill.name; - if (this.skills[skillName]) { - ++this.skills[skillName]; - } else { - this.skills[skillName] = 1; - } - if (isNaN(this.skills[skillName]) || this.skills[skillName] < 0) { - throw new Error("Level of Skill " + skillName + " is invalid: " + this.skills[skillName]); - } - this.updateSkillMultipliers(); -} - -Bladeburner.prototype.getActionObject = function(actionId) { - /** - * 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"]; - default: - return null; - } -} - -// Sets the player to the "IDLE" action -Bladeburner.prototype.resetAction = function() { - this.action = new ActionIdentifier({type:ActionTypes.Idle}); -} - -Bladeburner.prototype.startAction = function(actionId) { - if (actionId == null) {return;} - this.action = actionId; - this.actionTimeCurrent = 0; - switch (actionId.type) { - case ActionTypes["Idle"]: - this.actionTimeToComplete = 0; - break; - case ActionTypes["Contract"]: - try { - var 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); - } catch(e) { - exceptionAlert(e); - } - break; - case ActionTypes["Operation"]: - try { - var 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().commsEst === 0) {return this.resetAction();} - this.actionTimeToComplete = action.getActionTime(this); - } catch(e) { - exceptionAlert(e); - } - break; - case ActionTypes["BlackOp"]: - case ActionTypes["BlackOperation"]: - try { - // Safety measure - don't repeat BlackOps that are already done - if (this.blackops[actionId.name] != null) { - this.resetAction(); - this.log("Error: Tried to start a Black Operation that had already been completed"); - break; - } - - var action = this.getActionObject(actionId); - if (action == null) { - throw new Error("Failed to get BlackOperation object for: " + actionId.name); - } - this.actionTimeToComplete = action.getActionTime(this); - } catch(e) { - exceptionAlert(e); - } - break; - case ActionTypes["Recruitment"]: - this.actionTimeToComplete = this.getRecruitmentTime(); - break; - case ActionTypes["Training"]: - case ActionTypes["FieldAnalysis"]: - case ActionTypes["Field Analysis"]: - this.actionTimeToComplete = 30; - break; - case ActionTypes["Diplomacy"]: - case ActionTypes["Hyperbolic Regeneration Chamber"]: - this.actionTimeToComplete = 60; - break; - default: - throw new Error("Invalid Action Type in Bladeburner.startAction(): " + actionId.type); - break; - } -} - -Bladeburner.prototype.processAction = function(seconds) { - 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 instanceof ActionIdentifier)) { - throw new Error("Bladeburner.action is not an ActionIdentifier Object"); - } - - // 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) { - this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete; - return this.completeAction(); - } -} - -Bladeburner.prototype.completeAction = function() { - switch (this.action.type) { - case ActionTypes["Contract"]: - case ActionTypes["Operation"]: - try { - var isOperation = (this.action.type === ActionTypes["Operation"]); - var action = this.getActionObject(this.action); - if (action == null) { - throw new Error("Failed to get Contract/Operation Object for: " + this.action.name); - } - var difficulty = action.getDifficulty(); - var difficultyMultiplier = Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + difficulty / BladeburnerConstants.DiffMultLinearFactor; - var rewardMultiplier = Math.pow(action.rewardFac, action.level-1); - - // 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)) { - this.gainActionStats(action, true); - ++action.successes; - --action.count; - - // Earn money for contracts - var moneyGain = 0; - if (!isOperation) { - moneyGain = BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.skillMultipliers.money; - Player.gainMoney(moneyGain); - Player.recordMoneySource(moneyGain, "bladeburner"); - } - - if (isOperation) { - action.setMaxLevel(BladeburnerConstants.OperationSuccessesPerLevel); - } else { - action.setMaxLevel(BladeburnerConstants.ContractSuccessesPerLevel); - } - if (action.rankGain) { - var gain = addOffset(action.rankGain * rewardMultiplier * BitNodeMultipliers.BladeburnerRank, 10); - this.changeRank(gain); - if (isOperation && this.logging.ops) { - this.log(action.name + " successfully completed! Gained " + formatNumber(gain, 3) + " rank"); - } else if (!isOperation && this.logging.contracts) { - this.log(action.name + " contract successfully completed! Gained " + formatNumber(gain, 3) + " rank and " + numeralWrapper.formatMoney(moneyGain)); - } - } - isOperation ? this.completeOperation(true) : this.completeContract(true); - } else { - this.gainActionStats(action, false); - ++action.failures; - var loss = 0, damage = 0; - if (action.rankLoss) { - loss = addOffset(action.rankLoss * rewardMultiplier, 10); - this.changeRank(-1 * loss); - } - if (action.hpLoss) { - damage = action.hpLoss * difficultyMultiplier; - damage = Math.ceil(addOffset(damage, 10)); - this.hpLost += damage; - const cost = calculateHospitalizationCost(Player, damage); - if (Player.takeDamage(damage)) { - ++this.numHosp; - this.moneyLost += cost; - } - } - var logLossText = ""; - if (loss > 0) {logLossText += "Lost " + formatNumber(loss, 3) + " rank. ";} - if (damage > 0) {logLossText += "Took " + formatNumber(damage, 0) + " damage.";} - if (isOperation && this.logging.ops) { - this.log(action.name + " failed! " + logLossText); - } else if (!isOperation && this.logging.contracts) { - this.log(action.name + " contract failed! " + logLossText); - } - isOperation ? this.completeOperation(false) : this.completeContract(false); - } - if (action.autoLevel) {action.level = action.maxLevel;} // Autolevel - this.startAction(this.action); // Repeat action - } catch(e) { - exceptionAlert(e); - } - break; - case ActionTypes["BlackOp"]: - case ActionTypes["BlackOperation"]: - try { - var action = this.getActionObject(this.action); - if (action == null || !(action instanceof BlackOperation)) { - throw new Error("Failed to get BlackOperation Object for: " + this.action.name); - } - var difficulty = action.getDifficulty(); - var 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 - var teamCount = action.teamCount, teamLossMax; - - if (action.attempt(this)) { - this.gainActionStats(action, true); - action.count = 0; - this.blackops[action.name] = true; - var rankGain = 0; - if (action.rankGain) { - rankGain = addOffset(action.rankGain * BitNodeMultipliers.BladeburnerRank, 10); - this.changeRank(rankGain); - } - teamLossMax = Math.ceil(teamCount/2); - - // Operation Daedalus - if (action.name === "Operation Daedalus") { - this.resetAction(); - return hackWorldDaemon(Player.bitNodeN); - } - - if (routing.isOn(Page.Bladeburner)) { - this.createActionAndSkillsContent(); - } - - if (this.logging.blackops) { - this.log(action.name + " successful! Gained " + formatNumber(rankGain, 1) + " rank"); - } - } else { - this.gainActionStats(action, false); - var rankLoss = 0, damage = 0; - if (action.rankLoss) { - rankLoss = addOffset(action.rankLoss, 10); - this.changeRank(-1 * rankLoss); - } - if (action.hpLoss) { - damage = action.hpLoss * difficultyMultiplier; - damage = Math.ceil(addOffset(damage, 10)); - const cost = calculateHospitalizationCost(Player, damage); - if (Player.takeDamage(damage)) { - ++this.numHosp; - this.moneyLost += cost; - } - } - teamLossMax = Math.floor(teamCount); - - if (this.logging.blackops) { - this.log(action.name + " failed! Lost " + formatNumber(rankLoss, 1) + " rank and took " + formatNumber(damage, 0) + " damage"); - } - } - - this.resetAction(); // Stop regardless of success or fail - - // Calculate team lossses - if (teamCount >= 1) { - var losses = getRandomInt(1, teamLossMax); - this.teamSize -= losses; - this.teamLost += losses; - if (this.logging.blackops) { - this.log("You lost " + formatNumber(losses, 0) + " team members during " + action.name); - } - } - } catch(e) { - exceptionAlert(e); - } - break; - case ActionTypes["Training"]: - this.stamina -= (0.5 * BladeburnerConstants.BaseStaminaLoss); - var strExpGain = 30 * Player.strength_exp_mult, - defExpGain = 30 * Player.defense_exp_mult, - dexExpGain = 30 * Player.dexterity_exp_mult, - agiExpGain = 30 * Player.agility_exp_mult, - staminaGain = 0.04 * this.skillMultipliers.stamina; - Player.gainStrengthExp(strExpGain); - Player.gainDefenseExp(defExpGain); - Player.gainDexterityExp(dexExpGain); - Player.gainAgilityExp(agiExpGain); - this.staminaBonus += (staminaGain); - if (this.logging.general) { - this.log("Training completed. Gained: " + - 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"); - } - this.startAction(this.action); // Repeat action - break; - case ActionTypes["FieldAnalysis"]: - case ActionTypes["Field Analysis"]: - // Does not use stamina. Effectiveness depends on hacking, int, and cha - var eff = 0.04 * Math.pow(Player.hacking_skill, 0.3) + - 0.04 * Math.pow(Player.intelligence, 0.9) + - 0.02 * Math.pow(Player.charisma, 0.3); - eff *= Player.bladeburner_analysis_mult; - if (isNaN(eff) || eff < 0) { - throw new Error("Field Analysis Effectiveness calculated to be NaN or negative"); - } - var hackingExpGain = 20 * Player.hacking_exp_mult, - charismaExpGain = 20 * Player.charisma_exp_mult; - Player.gainHackingExp(hackingExpGain); - Player.gainIntelligenceExp(BladeburnerConstants.BaseIntGain); - Player.gainCharismaExp(charismaExpGain); - this.changeRank(0.1 * BitNodeMultipliers.BladeburnerRank); - this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate); - if (this.logging.general) { - this.log("Field analysis completed. Gained 0.1 rank, " + formatNumber(hackingExpGain, 1) + " hacking exp, and " + formatNumber(charismaExpGain, 1) + " charisma exp"); - } - this.startAction(this.action); // Repeat action - break; - case ActionTypes["Recruitment"]: - var successChance = this.getRecruitmentSuccessChance(); - if (Math.random() < successChance) { - var expGain = 2 * BladeburnerConstants.BaseStatGain * this.actionTimeToComplete; - Player.gainCharismaExp(expGain); - ++this.teamSize; - if (this.logging.general) { - this.log("Successfully recruited a team member! Gained " + formatNumber(expGain, 1) + " charisma exp"); - } - } else { - var expGain = BladeburnerConstants.BaseStatGain * this.actionTimeToComplete; - Player.gainCharismaExp(expGain); - if (this.logging.general) { - this.log("Failed to recruit a team member. Gained " + formatNumber(expGain, 1) + " charisma exp"); - } - } - this.startAction(this.action); // Repeat action - break; - case ActionTypes["Diplomacy"]: - var eff = this.getDiplomacyEffectiveness(); - this.getCurrentCity().chaos *= eff; - if (this.getCurrentCity().chaos < 0) { this.getCurrentCity().chaos = 0; } - if (this.logging.general) { - this.log(`Diplomacy completed. Chaos levels in the current city fell by ${numeralWrapper.formatPercentage(1 - eff)}`); - } - this.startAction(this.action); // Repeat Action - break; - case ActionTypes["Hyperbolic Regeneration Chamber"]: { - Player.regenerateHp(BladeburnerConstants.HrcHpGain); - - const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100); - this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain); - this.startAction(this.action); - if (this.logging.general) { - this.log(`Rested in Hyperbolic Regeneration Chamber. Restored ${BladeburnerConstants.HrcHpGain} HP and gained ${numeralWrapper.formatStamina(staminaGain)} stamina`); - } - break; - } - default: - console.error(`Bladeburner.completeAction() called for invalid action: ${this.action.type}`); - break; - } -} - -Bladeburner.prototype.completeContract = function(success) { - if (this.action.type !== ActionTypes.Contract) { - throw new Error("completeContract() called even though current action is not a Contract"); - } - var city = this.getCurrentCity(); - if (success) { - switch (this.action.name) { - case "Tracking": - // Increase estimate accuracy by a relatively small amount - city.improvePopulationEstimateByCount(getRandomInt(100, 1e3)); - break; - case "Bounty Hunter": - city.changePopulationByCount(-1, {estChange:-1}); - city.changeChaosByCount(0.02); - break; - case "Retirement": - city.changePopulationByCount(-1, {estChange:-1}); - city.changeChaosByCount(0.04); - break; - default: - throw new Error("Invalid Action name in completeContract: " + this.action.name); - } - } -} - -Bladeburner.prototype.completeOperation = function(success) { - if (this.action.type !== ActionTypes.Operation) { - throw new Error("completeOperation() called even though current action is not an Operation"); - } - var action = this.getActionObject(this.action); - if (action == null) { - throw new Error("Failed to get Contract/Operation Object for: " + this.action.name); - } - - // Calculate team losses - var teamCount = action.teamCount, max; - if (teamCount >= 1) { - if (success) { - max = Math.ceil(teamCount/2); - } else { - max = Math.floor(teamCount) - } - var losses = getRandomInt(0, max); - this.teamSize -= losses; - this.teamLost += losses; - if (this.logging.ops && losses > 0) { - this.log("Lost " + formatNumber(losses, 0) + " team members during this " + action.name); - } - } - - var city = this.getCurrentCity(); - switch (action.name) { - case "Investigation": - if (success) { - city.improvePopulationEstimateByPercentage(0.4 * this.skillMultipliers.successChanceEstimate); - if (Math.random() < (0.02 * this.skillMultipliers.successChanceEstimate)) { - city.improveCommunityEstimate(1); - } - } else { - this.triggerPotentialMigration(this.city, 0.1); - } - break; - case "Undercover Operation": - if (success) { - city.improvePopulationEstimateByPercentage(0.8 * this.skillMultipliers.successChanceEstimate); - if (Math.random() < (0.02 * this.skillMultipliers.successChanceEstimate)) { - city.improveCommunityEstimate(1); - } - } 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; - --city.commsEst; - } else { - var change = getRandomInt(-10, -5) / 10; - city.changePopulationByPercentage(change, {nonZero:true}); - } - 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}); - } - city.changeChaosByPercentage(getRandomInt(-5, 5)); - break; - default: - throw new Error("Invalid Action name in completeOperation: " + this.action.name); - } -} - -Bladeburner.prototype.getRecruitmentTime = function() { - var effCharisma = Player.charisma * this.skillMultipliers.effCha; - var charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90; - return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor)); -} - -Bladeburner.prototype.getRecruitmentSuccessChance = function() { - return Math.pow(Player.charisma, 0.45) / (this.teamSize + 1); -} - -Bladeburner.prototype.getDiplomacyEffectiveness = function() { - // 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(Player.charisma, CharismaExponentialFactor) + Player.charisma / CharismaLinearFactor; - return (100 - charismaEff) / 100; -} - -/** - * Process stat gains from Contracts, Operations, and Black Operations - * @param action(Action obj) - Derived action class - * @param success(bool) - Whether action was successful - */ -Bladeburner.prototype.gainActionStats = function(action, success) { - var difficulty = action.getDifficulty(); - - /** - * Gain multiplier based on difficulty. If this changes then the - * same variable calculated in completeAction() needs to change too - */ - var difficultyMult = Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + difficulty / BladeburnerConstants.DiffMultLinearFactor; - - var time = this.actionTimeToComplete; - var successMult = success ? 1 : 0.5; - - var unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult; - var unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult; - const skillMult = this.skillMultipliers.expGain; - Player.gainHackingExp(unweightedGain * action.weights.hack * Player.hacking_exp_mult * skillMult); - Player.gainStrengthExp(unweightedGain * action.weights.str * Player.strength_exp_mult * skillMult); - Player.gainDefenseExp(unweightedGain * action.weights.def * Player.defense_exp_mult * skillMult); - Player.gainDexterityExp(unweightedGain * action.weights.dex * Player.dexterity_exp_mult * skillMult); - Player.gainAgilityExp(unweightedGain * action.weights.agi * Player.agility_exp_mult * skillMult); - Player.gainCharismaExp(unweightedGain * action.weights.cha * Player.charisma_exp_mult * skillMult); - let intExp = unweightedIntGain * action.weights.int * skillMult; - if (intExp > 1) { - intExp = Math.pow(intExp, 0.8); - } - Player.gainIntelligenceExp(intExp); -} - -Bladeburner.prototype.randomEvent = function() { - var chance = Math.random(); - - // Choose random source/destination city for events - var sourceCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; - var sourceCity = this.cities[sourceCityName]; - if (!(sourceCity instanceof City)) { - throw new Error("sourceCity was not a City object in Bladeburner.randomEvent()"); - } - - var destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; - while (destCityName === sourceCityName) { - destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; - } - var destCity = this.cities[destCityName]; - - if (!(sourceCity instanceof City) || !(destCity instanceof City)) { - throw new Error("sourceCity/destCity was not a City object in Bladeburner.randomEvent()"); - } - - if (chance <= 0.05) { - // New Synthoid Community, 5% - ++sourceCity.comms; - var percentage = getRandomInt(10, 20) / 100; - var count = Math.round(sourceCity.pop * percentage); - sourceCity.pop += count; - 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; - var percentage = getRandomInt(10, 20) / 100; - var count = Math.round(sourceCity.pop * percentage); - sourceCity.pop += count; - 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 - var percentage = getRandomInt(10, 20) / 100; - var count = Math.round(sourceCity.pop * percentage); - sourceCity.pop -= count; - destCity.pop += count; - - 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% - var percentage = getRandomInt(8, 24) / 100; - var 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"); - } - } 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% - var percentage = getRandomInt(8, 20) / 100; - var 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 -} - -Bladeburner.prototype.triggerPotentialMigration = function(sourceCityName, chance) { - 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);} -} - -Bladeburner.prototype.triggerMigration = function(sourceCityName) { - var destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; - while (destCityName === sourceCityName) { - destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; - } - var destCity = this.cities[destCityName]; - var sourceCity = this.cities[sourceCityName]; - if (destCity == null || sourceCity == null) { - throw new Error("Failed to find City with name: " + destCityName); - } - var rand = Math.random(), 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; - } - var count = Math.round(sourceCity.pop * percentage); - sourceCity.pop -= count; - destCity.pop += count; -} - -let DomElems = {}; - -Bladeburner.prototype.initializeDomElementRefs = function() { - DomElems = { - bladeburnerDiv: null, - - // Main Divs - overviewConsoleParentDiv: null, - - overviewDiv: null, // Overview of stats that stays fixed on left - actionAndSkillsDiv: null, // Panel for different sections (contracts, ops, skills) - currentTab: null, // Contracts, Operations, Black Ops, Skills - - consoleDiv: null, - consoleTable: null, - consoleInputRow: null, // tr - consoleInputCell: null, // td - consoleInputHeader: null, // "> " - consoleInput: null, // Actual input element - - // Overview Content - overviewRank: null, - overviewStamina: null, - overviewStaminaHelpTip: null, - overviewGen1: null, // Stamina Penalty, Team, Hospitalized stats, current city - overviewEstPop: null, - overviewEstPopHelpTip: null, - overviewEstComms: null, - overviewChaos: null, - overviewSkillPoints: null, - overviewBonusTime: null, - overviewAugMults: null, - - // Actions and Skills Content - actionsAndSkillsDesc: null, - actionsAndSkillsList: null, // ul element of all UI elements in this panel - generalActions: {}, - contracts: {}, - operations: {}, - blackops: {}, - skills: {}, - skillPointsDisplay: null, - }; -} - -Bladeburner.prototype.createContent = function() { - DomElems.bladeburnerDiv = createElement("div", { - id:"bladeburner-container", position:"fixed", class:"generic-menupage-container", - }); - - // Parent Div for Overview and Console - DomElems.overviewConsoleParentDiv = createElement("div", { - height:"60%", display:"block", position:"relative", - }); - - // Overview and Action/Skill pane - DomElems.overviewDiv = createElement("div", { - width:"30%", display:"inline-block", border:"1px solid white", - }); - - DomElems.actionAndSkillsDiv = createElement("div", { - width:"70%", display:"block", - border:"1px solid white", margin:"6px", padding:"6px", - }); - - DomElems.currentTab = "general"; - - this.createOverviewContent(); - this.createActionAndSkillsContent(); - - // Console - DomElems.consoleDiv = createElement("div", { - class:"bladeburner-console-div", - clickListener:() => { - if (DomElems.consoleInput instanceof Element) { - DomElems.consoleInput.focus(); - } - return false; - }, - }); - DomElems.consoleTable = createElement("table", {class:"bladeburner-console-table"}); - DomElems.consoleInputRow = createElement("tr", {class:"bladeburner-console-input-row", id:"bladeburner-console-input-row"}); - DomElems.consoleInputCell = createElement("td", {class:"bladeburner-console-input-cell"}); - DomElems.consoleInputHeader = createElement("pre", {innerText:"> "}); - DomElems.consoleInput = createElement("input", { - type:"text", class:"bladeburner-console-input", tabIndex:1, - onfocus:() => {DomElems.consoleInput.value = DomElems.consoleInput.value}, - }); - - DomElems.consoleInputCell.appendChild(DomElems.consoleInputHeader); - DomElems.consoleInputCell.appendChild(DomElems.consoleInput); - DomElems.consoleInputRow.appendChild(DomElems.consoleInputCell); - DomElems.consoleTable.appendChild(DomElems.consoleInputRow); - DomElems.consoleDiv.appendChild(DomElems.consoleTable); - - DomElems.overviewConsoleParentDiv.appendChild(DomElems.overviewDiv); - DomElems.overviewConsoleParentDiv.appendChild(DomElems.consoleDiv); - DomElems.bladeburnerDiv.appendChild(DomElems.overviewConsoleParentDiv); - DomElems.bladeburnerDiv.appendChild(DomElems.actionAndSkillsDiv); - - - // legend - const legend = createElement("div") - legend.innerHTML = `${stealthIcon}= This action requires stealth, ${killIcon} = This action involves retirement` - DomElems.bladeburnerDiv.appendChild(legend); - - document.getElementById("entire-game-container").appendChild(DomElems.bladeburnerDiv); - - if (this.consoleLogs.length === 0) { - this.postToConsole("Bladeburner Console BETA"); - this.postToConsole("Type 'help' to see console commands"); - } else { - for (let i = 0; i < this.consoleLogs.length; ++i) { - this.postToConsole(this.consoleLogs[i], false); - } - } - - DomElems.consoleInput.focus(); -} - -Bladeburner.prototype.clearContent = function() { - if (DomElems.bladeburnerDiv instanceof Element) { - removeChildrenFromElement(DomElems.bladeburnerDiv); - removeElement(DomElems.bladeburnerDiv); - } - clearObject(DomElems); - this.initializeDomElementRefs(); -} - -Bladeburner.prototype.createOverviewContent = function() { - if (DomElems.overviewDiv == null) { - throw new Error("Bladeburner.createOverviewContent() called with DomElems.overviewDiv = null"); - } - - DomElems.overviewRank = createElement("p", { - innerText:"Rank: ", - display:"inline-block", - tooltip:"Your rank within the Bladeburner division", - }); - - DomElems.overviewStamina = createElement("p", { - display:"inline-block", - }); - - DomElems.overviewStaminaHelpTip = createElement("div", { - class:"help-tip", - innerText:"?", - clickListener: () => { - dialogBoxCreate("Performing actions will use up your stamina.

" + - "Your max stamina is determined primarily by your agility stat.

" + - "Your stamina gain rate is determined by both your agility and your " + - "max stamina. Higher max stamina leads to a higher gain rate.

" + - "Once your " + - "stamina falls below 50% of its max value, it begins to negatively " + - "affect the success rate of your contracts/operations. This penalty " + - "is shown in the overview panel. If the penalty is 15%, then this means " + - "your success rate would be multipled by 85% (100 - 15).

" + - "Your max stamina and stamina gain rate can also be increased by " + - "training, or through skills and Augmentation upgrades."); - }, - }); - - DomElems.overviewGen1 = createElement("p", { - display:"block", - }); - - DomElems.overviewEstPop = createElement("p", { - innerText:"Est. Synthoid Population: ", - display:"inline-block", - tooltip:"This is your Bladeburner division's estimate of how many Synthoids exist " + - "in your current city.", - }); - - DomElems.overviewEstPopHelpTip = createElement("div", { - innerText:"?", class:"help-tip", - clickListener:() => { - dialogBoxCreate("The success rate of your contracts/operations depends on " + - "the population of Synthoids in your current city. " + - "The success rate that is shown to you is only an estimate, " + - "and it is based on your Synthoid population estimate.

" + - "Therefore, it is important that this Synthoid population estimate " + - "is accurate so that you have a better idea of your " + - "success rate for contracts/operations. Certain " + - "actions will increase the accuracy of your population " + - "estimate.

" + - "The Synthoid populations of cities can change due to your " + - "actions or random events. If random events occur, they will " + - "be logged in the Bladeburner Console."); - }, - }); - - DomElems.overviewEstComms = createElement("p", { - innerText:"Est. Synthoid Communities: ", - display:"inline-block", - tooltip:"This is your Bladeburner divison's estimate of how many Synthoid " + - "communities exist in your current city.", - }); - - DomElems.overviewChaos = createElement("p", { - innerText:"City Chaos: ", - display:"inline-block", - tooltip:"The city's chaos level due to tensions and conflicts between humans and Synthoids. " + - "Having too high of a chaos level can make contracts and operations harder.", - }); - - DomElems.overviewBonusTime = createElement("p", { - innerText: "Bonus time: ", - display: "inline-block", - tooltip: "You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by browser). " + - "Bonus time makes the Bladeburner mechanic progress faster, up to 5x the normal speed.", - }); - DomElems.overviewSkillPoints = createElement("p", {display:"block"}); - - - DomElems.overviewAugMults = createElement("div", {display:"block"}); - - - DomElems.overviewDiv.appendChild(DomElems.overviewRank); - appendLineBreaks(DomElems.overviewDiv, 1); - DomElems.overviewDiv.appendChild(DomElems.overviewStamina); - DomElems.overviewDiv.appendChild(DomElems.overviewStaminaHelpTip); - DomElems.overviewDiv.appendChild(DomElems.overviewGen1); - DomElems.overviewDiv.appendChild(DomElems.overviewEstPop); - DomElems.overviewDiv.appendChild(DomElems.overviewEstPopHelpTip); - appendLineBreaks(DomElems.overviewDiv, 1); - DomElems.overviewDiv.appendChild(DomElems.overviewEstComms); - appendLineBreaks(DomElems.overviewDiv, 1); - DomElems.overviewDiv.appendChild(DomElems.overviewChaos); - appendLineBreaks(DomElems.overviewDiv, 2); - DomElems.overviewDiv.appendChild(DomElems.overviewBonusTime); - DomElems.overviewDiv.appendChild(DomElems.overviewSkillPoints); - appendLineBreaks(DomElems.overviewDiv, 1); - DomElems.overviewDiv.appendChild(DomElems.overviewAugMults); - - // Travel to new city button - appendLineBreaks(DomElems.overviewDiv, 1); - DomElems.overviewDiv.appendChild(createElement("a", { - innerHTML:"Travel", class:"a-link-button", display:"inline-block", - clickListener:() => { - var popupId = "bladeburner-travel-popup-cancel-btn"; - var popupArguments = []; - popupArguments.push(createElement("a", { // Cancel Button - innerText:"Cancel", class:"a-link-button", - clickListener:() => { - removeElementById(popupId); return false; - }, - })) - popupArguments.push(createElement("p", { // Info Text - innerText:"Travel to a different city for your Bladeburner " + - "activities. This does not cost any money. The city you are " + - "in for your Bladeburner duties does not affect " + - "your location in the game otherwise", - })); - for (var i = 0; i < BladeburnerConstants.CityNames.length; ++i) { - (function(inst, i) { - popupArguments.push(createElement("div", { - /** - * Reusing this css class...it adds a border and makes it - * so that background color changes when you hover - */ - class:"cmpy-mgmt-find-employee-option", - innerText:BladeburnerConstants.CityNames[i], - clickListener:() => { - inst.city = BladeburnerConstants.CityNames[i]; - removeElementById(popupId); - inst.updateOverviewContent(); - return false; - }, - })); - })(this, i); - } - createPopup(popupId, popupArguments); - }, - })); - - // Faction button - const bladeburnersFactionName = "Bladeburners"; - if (factionExists(bladeburnersFactionName)) { - var bladeburnerFac = Factions[bladeburnersFactionName]; - if (!(bladeburnerFac instanceof Faction)) { - throw new Error("Could not properly get Bladeburner Faction object in Bladeburner UI Overview Faction button"); - } - DomElems.overviewDiv.appendChild(createElement("a", { - innerText:"Faction", class:"a-link-button", display:"inline-block", - tooltip:"Apply to the Bladeburner Faction, or go to the faction page if you are already a member", - clickListener:() => { - if (bladeburnerFac.isMember) { - Engine.loadFactionContent(); - displayFactionContent(bladeburnersFactionName); - } else { - if (this.rank >= BladeburnerConstants.RankNeededForFaction) { - joinFaction(bladeburnerFac); - dialogBoxCreate("Congratulations! You were accepted into the Bladeburners faction"); - removeChildrenFromElement(DomElems.overviewDiv); - this.createOverviewContent(); - } else { - dialogBoxCreate("You need a rank of 25 to join the Bladeburners Faction!") - } - } - return false; - }, - })); - } - - DomElems.overviewDiv.appendChild(createElement("br")); - DomElems.overviewDiv.appendChild(createElement("br")); - - this.updateOverviewContent(); -} - -Bladeburner.prototype.createActionAndSkillsContent = function() { - if (DomElems.currentTab == null) {DomElems.currentTab = "general";} - - removeChildrenFromElement(DomElems.actionAndSkillsDiv); - clearObject(DomElems.generalActions); - clearObject(DomElems.contracts); - clearObject(DomElems.operations); - clearObject(DomElems.blackops); - clearObject(DomElems.skills); - - //Navigation buttons - var currTab = DomElems.currentTab.toLowerCase(); - var buttons = ["General", "Contracts", "Operations", "BlackOps", "Skills"]; - for (var i = 0; i < buttons.length; ++i) { - (function(buttons, i, inst, currTab) { - - DomElems.actionAndSkillsDiv.appendChild(createElement("a", { - innerText:buttons[i], - class:currTab === buttons[i].toLowerCase() ? "bladeburner-nav-button-inactive" : "bladeburner-nav-button", - clickListener:() => { - DomElems.currentTab = buttons[i].toLowerCase(); - inst.createActionAndSkillsContent(); - return false; - }, - })); - }) (buttons, i, this, currTab); - } - - // General info/description for each action - DomElems.actionsAndSkillsDesc = createElement("p", { - display:"block", margin:"4px", padding:"4px", - }); - - // List for actions/skills - removeChildrenFromElement(DomElems.actionsAndSkillsList); - DomElems.actionsAndSkillsList = createElement("ul"); - - switch(currTab) { - case "general": - this.createGeneralActionsContent(); - break; - case "contracts": - this.createContractsContent(); - break; - case "operations": - this.createOperationsContent(); - break; - case "blackops": - this.createBlackOpsContent(); - break; - case "skills": - this.createSkillsContent(); - break; - default: - throw new Error("Invalid value for DomElems.currentTab in Bladeburner.createActionAndSkillsContent"); - } - this.updateContent(); - - DomElems.actionAndSkillsDiv.appendChild(DomElems.actionsAndSkillsDesc); - DomElems.actionAndSkillsDiv.appendChild(DomElems.actionsAndSkillsList); -} - -Bladeburner.prototype.createGeneralActionsContent = function() { - if (DomElems.actionsAndSkillsList == null || DomElems.actionsAndSkillsDesc == null) { - throw new Error("Bladeburner.createGeneralActionsContent called with either " + - "DomElems.actionsAndSkillsList or DomElems.actionsAndSkillsDesc = null"); - } - - DomElems.actionsAndSkillsDesc.innerText = - "These are generic actions that will assist you in your Bladeburner " + - "duties. They will not affect your Bladeburner rank in any way." - - for (var actionName in GeneralActions) { - if (GeneralActions.hasOwnProperty(actionName)) { - DomElems.generalActions[actionName] = createElement("div", { - class:"bladeburner-action", name:actionName, - }); - DomElems.actionsAndSkillsList.appendChild(DomElems.generalActions[actionName]); - } - } -} - -Bladeburner.prototype.createContractsContent = function() { - if (DomElems.actionsAndSkillsList == null || DomElems.actionsAndSkillsDesc == null) { - throw new Error("Bladeburner.createContractsContent called with either " + - "DomElems.actionsAndSkillsList or DomElems.actionsAndSkillsDesc = null"); - } - - DomElems.actionsAndSkillsDesc.innerHTML = - "Complete contracts in order to increase your Bladeburner rank and earn money. " + - "Failing a contract will cause you to lose HP, which can lead to hospitalization.

" + - "You can unlock higher-level contracts by successfully completing them. " + - "Higher-level contracts are more difficult, but grant more rank, experience, and money."; - - for (var contractName in this.contracts) { - if (this.contracts.hasOwnProperty(contractName)) { - DomElems.contracts[contractName] = createElement("div", { - class:"bladeburner-action", name:contractName, - }); - DomElems.actionsAndSkillsList.appendChild(DomElems.contracts[contractName]); - } - } -} - -Bladeburner.prototype.createOperationsContent = function() { - if (DomElems.actionsAndSkillsList == null || DomElems.actionsAndSkillsDesc == null) { - throw new Error("Bladeburner.createOperationsContent called with either " + - "DomElems.actionsAndSkillsList or DomElems.actionsAndSkillsDesc = null"); - } - - DomElems.actionsAndSkillsDesc.innerHTML = - "Carry out operations for the Bladeburner division. " + - "Failing an operation will reduce your Bladeburner rank. It will also " + - "cause you to lose HP, which can lead to hospitalization. In general, " + - "operations are harder and more punishing than contracts, " + - "but are also more rewarding.

" + - "Operations can affect the chaos level and Synthoid population of your " + - "current city. The exact effects vary between different Operations.

" + - "For operations, you can use a team. You must first recruit team members. " + - "Having a larger team will improves your chances of success.

" + - "You can unlock higher-level operations by successfully completing them. " + - "Higher-level operations are more difficult, but grant more rank and experience."; - - for (var operationName in this.operations) { - if (this.operations.hasOwnProperty(operationName)) { - DomElems.operations[operationName] = createElement("div", { - class:"bladeburner-action", name:operationName, - }); - DomElems.actionsAndSkillsList.appendChild(DomElems.operations[operationName]); - } - } -} - -Bladeburner.prototype.createBlackOpsContent = function() { - - if (DomElems.actionsAndSkillsList == null || DomElems.actionsAndSkillsDesc == null) { - throw new Error("Bladeburner.createBlackOpsContent called with either " + - "DomElems.actionsAndSkillsList or DomElems.actionsAndSkillsDesc = null"); - } - - - DomElems.actionsAndSkillsDesc.innerHTML = - "Black Operations (Black Ops) are special, one-time covert operations. " + - "Each Black Op must be unlocked successively by completing " + - "the one before it.

" + - "Your ultimate goal to climb through the ranks of Bladeburners is to complete " + - "all of the Black Ops.

" + - "Like normal operations, you may use a team for Black Ops. Failing " + - "a black op will incur heavy HP and rank losses."; - - // Put Black Operations in sequence of required rank - var blackops = []; - for (var blackopName in BlackOperations) { - if (BlackOperations.hasOwnProperty(blackopName)) { - blackops.push(BlackOperations[blackopName]); - } - } - blackops.sort(function(a, b) { - return (a.reqdRank - b.reqdRank); - }); - - for (var i = blackops.length-1; i >= 0 ; --i) { - if (this.blackops[[blackops[i].name]] == null && i !== 0 && this.blackops[[blackops[i-1].name]] == null) {continue;} // If this one nor the next are completed then this isn't unlocked yet. - DomElems.blackops[blackops[i].name] = createElement("div", { - class:"bladeburner-action", name:blackops[i].name, - }); - DomElems.actionsAndSkillsList.appendChild(DomElems.blackops[blackops[i].name]); - } -} - -Bladeburner.prototype.createSkillsContent = function() { - if (DomElems.actionsAndSkillsList == null || DomElems.actionsAndSkillsDesc == null) { - throw new Error("Bladeburner.createSkillsContent called with either " + - "DomElems.actionsAndSkillsList or DomElems.actionsAndSkillsDesc = null"); - } - - // Display Current multipliers - DomElems.actionsAndSkillsDesc.innerHTML = - "You will gain one skill point every " + BladeburnerConstants.RanksPerSkillPoint + " ranks.

" + - "Note that when upgrading a skill, the benefit for that skill is additive. " + - "However, the effects of different skills with each other is multiplicative.

" - var multKeys = Object.keys(this.skillMultipliers); - for (var i = 0; i < multKeys.length; ++i) { - var mult = this.skillMultipliers[multKeys[i]]; - if (mult && mult !== 1) { - mult = formatNumber(mult, 3); - switch(multKeys[i]) { - case "successChanceAll": - DomElems.actionsAndSkillsDesc.innerHTML += "Total Success Chance: x" + mult + "
"; - break; - case "successChanceStealth": - DomElems.actionsAndSkillsDesc.innerHTML += "Stealth Success Chance: x" + mult + "
"; - break; - case "successChanceKill": - DomElems.actionsAndSkillsDesc.innerHTML += "Retirement Success Chance: x" + mult + "
"; - break; - case "successChanceContract": - DomElems.actionsAndSkillsDesc.innerHTML += "Contract Success Chance: x" + mult + "
"; - break; - case "successChanceOperation": - DomElems.actionsAndSkillsDesc.innerHTML += "Operation Success Chance: x" + mult + "
"; - break; - case "successChanceEstimate": - DomElems.actionsAndSkillsDesc.innerHTML += "Synthoid Data Estimate: x" + mult + "
"; - break; - case "actionTime": - DomElems.actionsAndSkillsDesc.innerHTML += "Action Time: x" + mult + "
"; - break; - case "effHack": - DomElems.actionsAndSkillsDesc.innerHTML += "Hacking Skill: x" + mult + "
"; - break; - case "effStr": - DomElems.actionsAndSkillsDesc.innerHTML += "Strength: x" + mult + "
"; - break; - case "effDef": - DomElems.actionsAndSkillsDesc.innerHTML += "Defense: x" + mult + "
"; - break; - case "effDex": - DomElems.actionsAndSkillsDesc.innerHTML += "Dexterity: x" + mult + "
"; - break; - case "effAgi": - DomElems.actionsAndSkillsDesc.innerHTML += "Agility: x" + mult + "
"; - break; - case "effCha": - DomElems.actionsAndSkillsDesc.innerHTML += "Charisma: x" + mult + "
"; - break; - case "effInt": - DomElems.actionsAndSkillsDesc.innerHTML += "Intelligence: x" + mult + "
"; - break; - case "stamina": - DomElems.actionsAndSkillsDesc.innerHTML += "Stamina: x" + mult + "
"; - break; - case "money": - DomElems.actionsAndSkillsDesc.innerHTML += "Contract Money: x" + mult + "
"; - break; - case "expGain": - DomElems.actionsAndSkillsDesc.innerHTML += "Exp Gain: x" + mult + "
"; - break; - default: - console.warn(`Unrecognized SkillMult Key: ${multKeys[i]}`); - break; - } - } - } - - // Skill Points - DomElems.skillPointsDisplay = createElement("p", { - innerHTML:"
Skill Points: " + formatNumber(this.skillPoints, 0) + "", - }); - DomElems.actionAndSkillsDiv.appendChild(DomElems.skillPointsDisplay); - - // UI Element for each skill - for (var skillName in Skills) { - if (Skills.hasOwnProperty(skillName)) { - DomElems.skills[skillName] = createElement("div", { - class:"bladeburner-action", name:skillName, - }); - DomElems.actionsAndSkillsList.appendChild(DomElems.skills[skillName]); - } - } -} - -Bladeburner.prototype.updateContent = function() { - this.updateOverviewContent(); - this.updateActionAndSkillsContent(); -} - -Bladeburner.prototype.updateOverviewContent = function() { - if (!routing.isOn(Page.Bladeburner)) {return;} - DomElems.overviewRank.childNodes[0].nodeValue = "Rank: " + formatNumber(this.rank, 2); - DomElems.overviewStamina.innerText = "Stamina: " + formatNumber(this.stamina, 3) + " / " + formatNumber(this.maxStamina, 3); - ReactDOM.render(<> - Stamina Penalty: {formatNumber((1-this.calculateStaminaPenalty())*100, 1)}%

- Team Size: {formatNumber(this.teamSize, 0)}
- Team Members Lost: {formatNumber(this.teamLost, 0)}

- Num Times Hospitalized: {this.numHosp}
- Money Lost From Hospitalizations: {Money(this.moneyLost)}

- Current City: {this.city}
- , DomElems.overviewGen1); - - DomElems.overviewEstPop.childNodes[0].nodeValue = "Est. Synthoid Population: " + numeralWrapper.formatPopulation(this.getCurrentCity().popEst); - DomElems.overviewEstComms.childNodes[0].nodeValue = "Est. Synthoid Communities: " + formatNumber(this.getCurrentCity().comms, 0); - DomElems.overviewChaos.childNodes[0].nodeValue = "City Chaos: " + formatNumber(this.getCurrentCity().chaos); - DomElems.overviewSkillPoints.innerText = "Skill Points: " + formatNumber(this.skillPoints, 0); - DomElems.overviewBonusTime.childNodes[0].nodeValue = "Bonus time: " + convertTimeMsToTimeElapsedString(this.storedCycles/BladeburnerConstants.CyclesPerSecond*1000); - ReactDOM.render(StatsTable([ - ["Aug. Success Chance mult: ", formatNumber(Player.bladeburner_success_chance_mult*100, 1) + "%"], - ["Aug. Max Stamina mult: ", formatNumber(Player.bladeburner_max_stamina_mult*100, 1) + "%"], - ["Aug. Stamina Gain mult: ", formatNumber(Player.bladeburner_stamina_gain_mult*100, 1) + "%"], - ["Aug. Field Analysis mult: ", formatNumber(Player.bladeburner_analysis_mult*100, 1) + "%"], - ]), DomElems.overviewAugMults); -} - -Bladeburner.prototype.updateActionAndSkillsContent = function() { - if (DomElems.currentTab == null) {DomElems.currentTab = "general";} - switch(DomElems.currentTab.toLowerCase()) { - case "general": - var actionElems = Object.keys(DomElems.generalActions); - for (var i = 0; i < actionElems.length; ++i) { - var actionElem = DomElems.generalActions[actionElems[i]]; - var name = actionElem.name; - var actionObj = GeneralActions[name]; - if (actionObj == null) { - throw new Error("Could not find Object " + name + " in Bladeburner.updateActionAndSkillsContent()"); - } - if (this.action.type === ActionTypes[name]) { - actionElem.classList.add(ActiveActionCssClass); - } else { - actionElem.classList.remove(ActiveActionCssClass); - } - this.updateGeneralActionsUIElement(actionElem, actionObj); - } - break; - case "contracts": - var contractElems = Object.keys(DomElems.contracts); - for (var i = 0; i < contractElems.length; ++i) { - var contractElem = DomElems.contracts[contractElems[i]]; - var name = contractElem.name; - if (this.action.type === ActionTypes["Contract"] && name === this.action.name) { - contractElem.classList.add(ActiveActionCssClass); - } else { - contractElem.classList.remove(ActiveActionCssClass); - } - var contract = this.contracts[name]; - if (contract == null) { - throw new Error("Could not find Contract " + name + " in Bladeburner.updateActionAndSkillsContent()"); - } - this.updateContractsUIElement(contractElem, contract); - } - break; - case "operations": - var operationElems = Object.keys(DomElems.operations); - for (var i = 0; i < operationElems.length; ++i) { - var operationElem = DomElems.operations[operationElems[i]]; - var name = operationElem.name; - if (this.action.type === ActionTypes["Operation"] && name === this.action.name) { - operationElem.classList.add(ActiveActionCssClass); - } else { - operationElem.classList.remove(ActiveActionCssClass); - } - var operation = this.operations[name]; - if (operation == null) { - throw new Error("Could not find Operation " + name + " in Bladeburner.updateActionAndSkillsContent()"); - } - this.updateOperationsUIElement(operationElem, operation); - } - break; - case "blackops": - var blackopsElems = Object.keys(DomElems.blackops); - for (var i = 0; i < blackopsElems.length; ++i) { - var blackopElem = DomElems.blackops[blackopsElems[i]]; - var name = blackopElem.name; - if (this.action.type === ActionTypes["BlackOperation"] && name === this.action.name) { - blackopElem.classList.add(ActiveActionCssClass); - } else { - blackopElem.classList.remove(ActiveActionCssClass); - } - var blackop = BlackOperations[name]; - if (blackop == null) { - throw new Error("Could not find BlackOperation " + name + " in Bladeburner.updateActionAndSkillsContent()"); - } - this.updateBlackOpsUIElement(blackopElem, blackop); - } - break; - case "skills": - DomElems.skillPointsDisplay.innerHTML = "
Skill Points: " + formatNumber(this.skillPoints, 0) + ""; - - var skillElems = Object.keys(DomElems.skills); - for (var i = 0; i < skillElems.length; ++i) { - var skillElem = DomElems.skills[skillElems[i]]; - var name = skillElem.name; - var skill = Skills[name]; - if (skill == null) { - throw new Error("Could not find Skill " + name + " in Bladeburner.updateActionAndSkillsContent()"); - } - this.updateSkillsUIElement(skillElem, skill); - } - break; - default: - throw new Error("Invalid value for DomElems.currentTab in Bladeburner.createActionAndSkillsContent"); - } -} - -Bladeburner.prototype.updateGeneralActionsUIElement = function(el, action) { - removeChildrenFromElement(el); - var isActive = el.classList.contains(ActiveActionCssClass); - var computedActionTimeCurrent = Math.min(this.actionTimeCurrent+this.actionTimeOverflow,this.actionTimeToComplete); - - el.appendChild(createElement("h2", { // Header - innerText:isActive ? action.name + " (IN PROGRESS - " + - formatNumber(computedActionTimeCurrent, 0) + " / " + - formatNumber(this.actionTimeToComplete, 0) + ")" - : action.name, - display:"inline-block", - })); - - if (isActive) { // Progress bar if its active - var progress = computedActionTimeCurrent / this.actionTimeToComplete; - el.appendChild(createElement("p", { - display:"block", - innerText:createProgressBarText({progress:progress}), - })); - } else { - // Start button - el.appendChild(createElement("a", { - innerText:"Start", class: "a-link-button", - margin:"3px", padding:"3px", - clickListener:() => { - this.action.type = ActionTypes[action.name]; - this.action.name = action.name; - this.startAction(this.action); - this.updateActionAndSkillsContent(); - return false; - }, - })); - } - - appendLineBreaks(el, 2); - el.appendChild(createElement("pre", { // Info - innerHTML:action.desc, display:"inline-block", - })); - - -} - -Bladeburner.prototype.updateContractsUIElement = function(el, action) { - removeChildrenFromElement(el); - var isActive = el.classList.contains(ActiveActionCssClass); - var estimatedSuccessChance = action.getSuccessChance(this, {est:true}); - var computedActionTimeCurrent = Math.min(this.actionTimeCurrent+this.actionTimeOverflow,this.actionTimeToComplete); - - el.appendChild(createElement("h2", { // Header - innerText:isActive ? action.name + " (IN PROGRESS - " + - formatNumber(computedActionTimeCurrent, 0) + " / " + - formatNumber(this.actionTimeToComplete, 0) + ")" - : action.name, - display:"inline-block", - })); - - if (isActive) { // Progress bar if its active - var progress = computedActionTimeCurrent / this.actionTimeToComplete; - el.appendChild(createElement("p", { - display:"block", - innerText:createProgressBarText({progress:progress}), - })); - } else { // Start button - el.appendChild(createElement("a", { - innerText:"Start", class: "a-link-button", - padding:"3px", margin:"3px", - clickListener:() => { - this.action.type = ActionTypes.Contract; - this.action.name = action.name; - this.startAction(this.action); - this.updateActionAndSkillsContent(); - return false; - }, - })); - } - - // Level and buttons to change level - var maxLevel = (action.level >= action.maxLevel); - appendLineBreaks(el, 2); - el.appendChild(createElement("pre", { - display:"inline-block", - innerText:"Level: " + action.level + " / " + action.maxLevel, - tooltip:action.getSuccessesNeededForNextLevel(BladeburnerConstants.ContractSuccessesPerLevel) + " successes " + - "needed for next level", - })); - el.appendChild(createElement("a", { - class: maxLevel ? "a-link-button-inactive" : "a-link-button", innerHTML:"↑", - padding:"2px", margin:"2px", - tooltip: isActive ? "WARNING: changing the level will restart the contract" : "", - display:"inline", - clickListener:() => { - ++action.level; - if (isActive) {this.startAction(this.action);} // Restart Action - this.updateContractsUIElement(el, action); - return false; - }, - })); - el.appendChild(createElement("a", { - class: (action.level <= 1) ? "a-link-button-inactive" : "a-link-button", innerHTML:"↓", - padding:"2px", margin:"2px", - tooltip: isActive ? "WARNING: changing the level will restart the contract" : "", - display:"inline", - clickListener:() => { - --action.level; - if (isActive) {this.startAction(this.action);} // Restart Action - this.updateContractsUIElement(el, action); - return false; - }, - })); - - var actionTime = action.getActionTime(this); - appendLineBreaks(el, 2); - el.appendChild(createElement("pre", { // Info - display:"inline-block", - innerHTML:action.desc + "\n\n" + - `Estimated success chance: ${formatNumber(estimatedSuccessChance*100, 1)}% ${action.isStealth?stealthIcon:''}${action.isKill?killIcon:''}\n` + - "Time Required: " + convertTimeMsToTimeElapsedString(actionTime*1000) + "\n" + - "Contracts remaining: " + Math.floor(action.count) + "\n" + - "Successes: " + action.successes + "\n" + - "Failures: " + action.failures, - })); - - // Autolevel Checkbox - el.appendChild(createElement("br")); - var autolevelCheckboxId = "bladeburner-" + action.name + "-autolevel-checkbox"; - el.appendChild(createElement("label", { - for:autolevelCheckboxId, innerText:"Autolevel: ",color:"white", - tooltip:"Automatically increase contract level when possible", - })); - - const checkboxInput = createElement("input", { - type:"checkbox", - id: autolevelCheckboxId, - checked: action.autoLevel, - changeListener: () => { - action.autoLevel = checkboxInput.checked; - }, - }); - - el.appendChild(checkboxInput); -} - -Bladeburner.prototype.updateOperationsUIElement = function(el, action) { - removeChildrenFromElement(el); - var isActive = el.classList.contains(ActiveActionCssClass); - var estimatedSuccessChance = action.getSuccessChance(this, {est:true}); - var computedActionTimeCurrent = Math.min(this.actionTimeCurrent+this.actionTimeOverflow,this.actionTimeToComplete); - - el.appendChild(createElement("h2", { // Header - innerText:isActive ? action.name + " (IN PROGRESS - " + - formatNumber(computedActionTimeCurrent, 0) + " / " + - formatNumber(this.actionTimeToComplete, 0) + ")" - : action.name, - display:"inline-block", - })); - - if (isActive) { // Progress bar if its active - var progress = computedActionTimeCurrent / this.actionTimeToComplete; - el.appendChild(createElement("p", { - display:"block", - innerText:createProgressBarText({progress:progress}), - })); - } else { // Start button and set Team Size button - el.appendChild(createElement("a", { - innerText:"Start", class: "a-link-button", - margin:"3px", padding:"3px", - clickListener:() => { - this.action.type = ActionTypes.Operation; - this.action.name = action.name; - this.startAction(this.action); - this.updateActionAndSkillsContent(); - return false; - }, - })); - el.appendChild(createElement("a", { - innerText:"Set Team Size (Curr Size: " + formatNumber(action.teamCount, 0) + ")", class:"a-link-button", - margin:"3px", padding:"3px", - clickListener:() => { - var popupId = "bladeburner-operation-set-team-size-popup"; - var txt = createElement("p", { - innerText:"Enter the amount of team members you would like to take on these " + - "operations. If you do not have the specified number of team members, " + - "then as many as possible will be used. Note that team members may " + - "be lost during operations.", - - }); - var input = createElement("input", { - type:"number", placeholder: "Team size", class: "text-input", - }); - var setBtn = createElement("a", { - innerText:"Confirm", class:"a-link-button", - clickListener:() => { - var num = Math.round(parseFloat(input.value)); - if (isNaN(num) || num < 0) { - dialogBoxCreate("Invalid value entered for number of Team Members (must be numeric, positive)") - } else { - action.teamCount = num; - this.updateOperationsUIElement(el, action); - } - removeElementById(popupId); - return false; - }, - }); - var cancelBtn = createElement("a", { - innerText:"Cancel", class:"a-link-button", - clickListener:() => { - removeElementById(popupId); - return false; - }, - }); - createPopup(popupId, [txt, input, setBtn, cancelBtn]); - input.focus(); - }, - })); - } - - // Level and buttons to change level - var maxLevel = (action.level >= action.maxLevel); - appendLineBreaks(el, 2); - el.appendChild(createElement("pre", { - display:"inline-block", - innerText:"Level: " + action.level + " / " + action.maxLevel, - tooltip:action.getSuccessesNeededForNextLevel(BladeburnerConstants.OperationSuccessesPerLevel) + " successes " + - "needed for next level", - })); - el.appendChild(createElement("a", { - class: maxLevel ? "a-link-button-inactive" : "a-link-button", innerHTML:"↑", - padding:"2px", margin:"2px", - tooltip: isActive ? "WARNING: changing the level will restart the Operation" : "", - display:"inline", - clickListener:() => { - ++action.level; - if (isActive) {this.startAction(this.action);} // Restart Action - this.updateOperationsUIElement(el, action); - return false; - }, - })); - el.appendChild(createElement("a", { - class: (action.level <= 1) ? "a-link-button-inactive" : "a-link-button", innerHTML:"↓", - padding:"2px", margin:"2px", - tooltip: isActive ? "WARNING: changing the level will restart the Operation" : "", - display:"inline", - clickListener:() => { - --action.level; - if (isActive) {this.startAction(this.action);} // Restart Action - this.updateOperationsUIElement(el, action); - return false; - }, - })); - - // General Info - var actionTime = action.getActionTime(this); - appendLineBreaks(el, 2); - el.appendChild(createElement("pre", { - display:"inline-block", - innerHTML:action.desc + "\n\n" + - `Estimated success chance: ${formatNumber(estimatedSuccessChance*100, 1)}% ${action.isStealth?stealthIcon:''}${action.isKill?killIcon:''}\n` + - "Time Required: " + convertTimeMsToTimeElapsedString(actionTime*1000) + "\n" + - "Operations remaining: " + Math.floor(action.count) + "\n" + - "Successes: " + action.successes + "\n" + - "Failures: " + action.failures, - })); - - // Autolevel Checkbox - el.appendChild(createElement("br")); - var autolevelCheckboxId = "bladeburner-" + action.name + "-autolevel-checkbox"; - el.appendChild(createElement("label", { - for:autolevelCheckboxId, innerText:"Autolevel: ",color:"white", - tooltip:"Automatically increase operation level when possible", - })); - - const checkboxInput = createElement("input", { - type:"checkbox", - id: autolevelCheckboxId, - checked: action.autoLevel, - changeListener: () => { - action.autoLevel = checkboxInput.checked; - }, - }); - - el.appendChild(checkboxInput); -} - -Bladeburner.prototype.updateBlackOpsUIElement = function(el, action) { - removeChildrenFromElement(el); - var isActive = el.classList.contains(ActiveActionCssClass); - var isCompleted = (this.blackops[action.name] != null); - var estimatedSuccessChance = action.getSuccessChance(this, {est:true}); - var actionTime = action.getActionTime(this); - var hasReqdRank = this.rank >= action.reqdRank; - var computedActionTimeCurrent = Math.min(this.actionTimeCurrent+this.actionTimeOverflow,this.actionTimeToComplete); - - // UI for Completed Black Op - if (isCompleted) { - el.appendChild(createElement("h2", { - innerText:action.name + " (COMPLETED)", display:"block", - })); - return; - } - - el.appendChild(createElement("h2", { // Header - innerText:isActive ? action.name + " (IN PROGRESS - " + - formatNumber(computedActionTimeCurrent, 0) + " / " + - formatNumber(this.actionTimeToComplete, 0) + ")" - : action.name, - display:"inline-block", - })); - - if (isActive) { // Progress bar if its active - var progress = computedActionTimeCurrent / this.actionTimeToComplete; - el.appendChild(createElement("p", { - display:"block", - innerText:createProgressBarText({progress:progress}), - })); - } else { - el.appendChild(createElement("a", { // Start button - innerText:"Start", margin:"3px", padding:"3px", - class:hasReqdRank ? "a-link-button" : "a-link-button-inactive", - clickListener:() => { - this.action.type = ActionTypes.BlackOperation; - this.action.name = action.name; - this.startAction(this.action); - this.updateActionAndSkillsContent(); - return false; - }, - })); - el.appendChild(createElement("a", { // Set Team Size Button - innerText:"Set Team Size (Curr Size: " + formatNumber(action.teamCount, 0) + ")", class:"a-link-button", - margin:"3px", padding:"3px", - clickListener:() => { - var popupId = "bladeburner-operation-set-team-size-popup"; - var txt = createElement("p", { - innerText:"Enter the amount of team members you would like to take on this " + - "BlackOp. If you do not have the specified number of team members, " + - "then as many as possible will be used. Note that team members may " + - "be lost during operations.", - - }); - var input = createElement("input", { - type:"number", placeholder: "Team size", class: "text-input", - }); - var setBtn = createElement("a", { - innerText:"Confirm", class:"a-link-button", - clickListener:() => { - var num = Math.round(parseFloat(input.value)); - if (isNaN(num) || num < 0) { - dialogBoxCreate("Invalid value entered for number of Team Members (must be numeric, positive)") - } else { - action.teamCount = num; - this.updateBlackOpsUIElement(el, action); - } - removeElementById(popupId); - return false; - }, - }); - var cancelBtn = createElement("a", { - innerText:"Cancel", class:"a-link-button", - clickListener:() => { - removeElementById(popupId); - return false; - }, - }); - createPopup(popupId, [txt, input, setBtn, cancelBtn]); - input.focus(); - }, - })); - } - - // Info - appendLineBreaks(el, 2); - el.appendChild(createElement("p", { - display:"inline-block", - innerHTML:"
" + action.desc + "

", - })); - el.appendChild(createElement("p", { - display:"block", color:hasReqdRank ? "white" : "red", - innerHTML:"Required Rank: " + formatNumber(action.reqdRank, 0) + "
", - })); - el.appendChild(createElement("p", { - display:"inline-block", - innerHTML:`Estimated Success Chance: ${formatNumber(estimatedSuccessChance*100, 1)}% ${action.isStealth?stealthIcon:''}${action.isKill?killIcon:''}\n` + - "Time Required: " + convertTimeMsToTimeElapsedString(actionTime*1000), - })) -} - -Bladeburner.prototype.updateSkillsUIElement = function(el, skill) { - removeChildrenFromElement(el); - var skillName = skill.name; - var currentLevel = 0; - if (this.skills[skillName] && !isNaN(this.skills[skillName])) { - currentLevel = this.skills[skillName]; - } - var pointCost = skill.calculateCost(currentLevel); - - const nameDiv = createElement("div"); - ReactDOM.render(React.createElement(CopyableText, {value: skill.name}, null), nameDiv); - el.appendChild(nameDiv) - - const h2 = createElement("h2", { // Header - display:"inline-block", - }); - h2.appendChild(nameDiv); - el.appendChild(h2); - - var canLevel = this.skillPoints >= pointCost; - var maxLvl = skill.maxLvl ? currentLevel >= skill.maxLvl : false; - el.appendChild(createElement("a", { // Level up button - innerText:"Level", display:"inline-block", - class: canLevel && !maxLvl ? "a-link-button" : "a-link-button-inactive", - margin:"3px", padding:"3px", - clickListener:() => { - if (this.skillPoints < pointCost) {return;} - this.skillPoints -= pointCost; - this.upgradeSkill(skill); - this.createActionAndSkillsContent(); - return false; - }, - })); - appendLineBreaks(el, 2); - el.appendChild(createElement("p", { - display:"block", - innerText:`Level: ${currentLevel}`, - })); - if (maxLvl) { - el.appendChild(createElement("p", { - color:"red", display:"block", - innerText:"MAX LEVEL", - })); - } else { - el.appendChild(createElement("p", { - display:"block", - innerText:"Skill Points required: " + formatNumber(pointCost, 0), - })); - } - el.appendChild(createElement("p", { // Info/Description - innerHTML:skill.desc, display:"inline-block", - })); -} - -// Bladeburner Console Window -Bladeburner.prototype.postToConsole = function(input, saveToLogs=true) { - const MaxConsoleEntries = 100; - if (saveToLogs === true) { - this.consoleLogs.push(input); - if (this.consoleLogs.length > MaxConsoleEntries) { - this.consoleLogs.shift(); - } - } - - if (input == null || DomElems.consoleDiv == null) {return;} - $("#bladeburner-console-input-row").before('' + input + ''); - - if (DomElems.consoleTable.childNodes.length > MaxConsoleEntries) { - DomElems.consoleTable.removeChild(DomElems.consoleTable.firstChild); - } - - this.updateConsoleScroll(); -} - -Bladeburner.prototype.updateConsoleScroll = function() { - DomElems.consoleDiv.scrollTop = DomElems.consoleDiv.scrollHeight; -} - -Bladeburner.prototype.resetConsoleInput = function() { - DomElems.consoleInput.value = ""; -} - -Bladeburner.prototype.clearConsole = function() { - while (DomElems.consoleTable.childNodes.length > 1) { - DomElems.consoleTable.removeChild(DomElems.consoleTable.firstChild); - } - - this.consoleLogs.length = 0; -} - -Bladeburner.prototype.log = function(input) { - // Adds a timestamp and then just calls postToConsole - this.postToConsole(`[${getTimestamp()}] ${input}`); -} - -// Handles a potential series of commands (comm1; comm2; comm3;) -Bladeburner.prototype.executeConsoleCommands = function(commands) { - 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); - } - } - consoleHistoryIndex = this.consoleHistory.length; - - const arrayOfCommands = commands.split(";"); - for (let i = 0; i < arrayOfCommands.length; ++i) { - this.executeConsoleCommand(arrayOfCommands[i]); - } - } catch(e) { - exceptionAlert(e); - } -} - -// Execute a single console command -Bladeburner.prototype.executeConsoleCommand = function(command) { - command = command.trim(); - command = command.replace(/\s\s+/g, ' '); // Replace all whitespace w/ a single space - - var 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; - } -} - -Bladeburner.prototype.parseCommandArguments = function(command) { - /** - * 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 this fn will be trimmed and will have all whitespace replaced w/ a single space - */ - const args = []; - let start = 0, i = 0; - while (i < command.length) { - const c = command.charAt(i); - if (c === '"') { // Double quotes - const endQuote = command.indexOf('"', i+1); - if (endQuote !== -1 && (endQuote === command.length-1 || command.charAt(endQuote+1) === " ")) { - 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); - if (endQuote !== -1 && (endQuote === command.length-1 || command.charAt(endQuote+1) === " ")) { - 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 === " ") { - args.push(command.substr(start, i-start)); - start = i+1; - } - ++i; - } - if (start !== i) {args.push(command.substr(start, i-start));} - return args; -} - -Bladeburner.prototype.executeAutomateConsoleCommand = function(args) { - 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) { - var flag = args[1]; - if (flag.toLowerCase() === "status") { - this.postToConsole("Automation: " + (this.automateEnabled ? "enabled" : "disabled")); - if (this.automateEnabled) { - this.postToConsole("When your stamina drops to " + 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 + "."); - } - - } 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; - } - - // Set variables - if (args.length === 4) { - var variable = args[1], val = args[2]; - - var 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) { - var 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) { - var 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) { - var 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(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; - } -} - -Bladeburner.prototype.executeHelpConsoleCommand = function(args) { - if (args.length === 1) { - for(const line of ConsoleHelpText.helpList){ - this.postToConsole(line); - } - } else { - for (var i = 1; i < args.length; ++i) { - const helpText = ConsoleHelpText[args[i]]; - for(const line of helpText){ - this.postToConsole(line); - } - } - } -} - -Bladeburner.prototype.executeLogConsoleCommand = function(args) { - 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; - } - - var 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; - } -} - -Bladeburner.prototype.executeSkillConsoleCommand = function(args) { - 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: "); - var skillNames = Object.keys(Skills); - for(var i = 0; i < skillNames.length; ++i) { - var skill = Skills[skillNames[i]]; - var level = 0; - if (this.skills[skill.name] != null) {level = this.skills[skill.name];} - this.postToConsole(skill.name + ": Level " + formatNumber(level, 0)); - } - this.postToConsole(" "); - this.postToConsole("Effects: "); - var multKeys = Object.keys(this.skillMultipliers); - for (var i = 0; i < multKeys.length; ++i) { - var mult = this.skillMultipliers[multKeys[i]]; - if (mult && mult !== 1) { - mult = formatNumber(mult, 3); - switch(multKeys[i]) { - case "successChanceAll": - this.postToConsole("Total Success Chance: x" + mult); - break; - case "successChanceStealth": - this.postToConsole("Stealth Success Chance: x" + mult); - break; - case "successChanceKill": - this.postToConsole("Retirement Success Chance: x" + mult); - break; - case "successChanceContract": - this.postToConsole("Contract Success Chance: x" + mult); - break; - case "successChanceOperation": - this.postToConsole("Operation Success Chance: x" + mult); - break; - case "successChanceEstimate": - this.postToConsole("Synthoid Data Estimate: x" + mult); - break; - case "actionTime": - this.postToConsole("Action Time: x" + mult); - break; - case "effHack": - this.postToConsole("Hacking Skill: x" + mult); - break; - case "effStr": - this.postToConsole("Strength: x" + mult); - break; - case "effDef": - this.postToConsole("Defense: x" + mult); - break; - case "effDex": - this.postToConsole("Dexterity: x" + mult); - break; - case "effAgi": - this.postToConsole("Agility: x" + mult); - break; - case "effCha": - this.postToConsole("Charisma: x" + mult); - break; - case "effInt": - this.postToConsole("Intelligence: x" + mult); - break; - case "stamina": - this.postToConsole("Stamina: x" + mult); - 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: - var skillName = args[2]; - var skill = Skills[skillName]; - if (skill == null || !(skill instanceof Skill)) { - return this.postToConsole("Invalid skill name (Note that this is case-sensitive): " + skillName); - } - 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), 0); - } else if (args[1].toLowerCase() === "level") { - var currentLevel = 0; - if (this.skills[skillName] && !isNaN(this.skills[skillName])) { - currentLevel = this.skills[skillName]; - } - var pointCost = skill.calculateCost(currentLevel); - if (this.skillPoints >= pointCost) { - this.skillPoints -= pointCost; - this.upgradeSkill(skill); - this.log(skill.name + " upgraded to Level " + this.skills[skillName]); - this.createActionAndSkillsContent(); - } else { - this.postToConsole("You do not have enough Skill Points to upgrade this. You need " + formatNumber(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; - } -} - -Bladeburner.prototype.executeStartConsoleCommand = function(args) { - if (args.length !== 3) { - this.postToConsole("Invalid usage of 'start' console command: start [type] [name]"); - this.postToConsole("Use 'help start' for more info"); - return; - } - var 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); - this.updateActionAndSkillsContent(); - } 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); - this.updateActionAndSkillsContent(); - } 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); - this.updateActionAndSkillsContent(); - } 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); - this.updateActionAndSkillsContent(); - } 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; - } -} - -Bladeburner.prototype.getActionIdFromTypeAndName = function(type="", name="") { - if (type === "" || name === "") {return null;} - var action = new ActionIdentifier(); - var convertedType = type.toLowerCase().trim(); - var 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; - } else { - return null; - } - break; - case "operation": - case "operations": - case "op": - case "ops": - action.type = ActionTypes["Operation"]; - if (this.operations.hasOwnProperty(name)) { - action.name = name; - return action; - } else { - return null; - } - break; - 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; - } else { - return null; - } - break; - 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; - default: - return null; - } - return action; - } -} - -Bladeburner.prototype.getTypeAndNameFromActionId = function(actionId) { - var res = {}; - let types = Object.keys(ActionTypes); - for (let i = 0; i < types.length; ++i) { - if (actionId.type === ActionTypes[types[i]]) { - res.type = types[i]; - break; - } - } - if (res.type == null) {res.type = "Idle";} - - res.name = actionId.name != null ? actionId.name : "Idle"; - return res; -} - -Bladeburner.prototype.getContractNamesNetscriptFn = function() { - return Object.keys(this.contracts); -} - -Bladeburner.prototype.getOperationNamesNetscriptFn = function() { - return Object.keys(this.operations); -} - -Bladeburner.prototype.getBlackOpNamesNetscriptFn = function() { - return Object.keys(BlackOperations); -} - -Bladeburner.prototype.getGeneralActionNamesNetscriptFn = function() { - return Object.keys(GeneralActions); -} - -Bladeburner.prototype.getSkillNamesNetscriptFn = function() { - return Object.keys(Skills); -} - -Bladeburner.prototype.startActionNetscriptFn = function(type, name, workerScript) { - 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"]) { - // Can't start a BlackOp if you don't have the required rank - let action = this.getActionObject(actionId); - if (action.reqdRank > this.rank) { - workerScript.log("bladeburner.startAction", `Insufficient rank to start Black Op '${actionId.name}'.`); - return false; - } - - // Can't start a BlackOp if its already been done - if (this.blackops[actionId.name] != null) { - workerScript.log("bladeburner.startAction", `Black Op ${actionId.name} has already been completed.`); - return false; - } - - // Can't start a BlackOp if you haven't done the one before it - var blackops = []; - for (const nm in 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 - }); - - let i = blackops.indexOf(actionId.name); - if (i === -1) { - workerScript.log("bladeburner.startAction", `Invalid Black Op: '${name}'`); - return false; - } - - if (i > 0 && this.blackops[blackops[i-1]] == null) { - workerScript.log("bladeburner.startAction", `Preceding Black Op must be completed before starting '${actionId.name}'.`); - return false; - } - } - - try { - this.startAction(actionId); - workerScript.log("bladeburner.startAction", `Starting bladeburner action with type '${type}' and name ${name}"`); - return true; - } catch(e) { - this.resetAction(); - workerScript.log("bladeburner.startAction", errorLogText); - return false; - } -} - -Bladeburner.prototype.getActionTimeNetscriptFn = function(type, name, workerScript) { - const errorLogText = `Invalid action: type='${type}' name='${name}'` - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - workerScript.log("bladeburner.getActionTime", errorLogText); - return -1; - } - - const actionObj = this.getActionObject(actionId); - if (actionObj == null) { - workerScript.log("bladeburner.getActionTime", errorLogText); - return -1; - } - - switch (actionId.type) { - case ActionTypes["Contract"]: - case ActionTypes["Operation"]: - case ActionTypes["BlackOp"]: - case ActionTypes["BlackOperation"]: - return actionObj.getActionTime(this); - case ActionTypes["Training"]: - case ActionTypes["Field Analysis"]: - case ActionTypes["FieldAnalysis"]: - return 30; - case ActionTypes["Recruitment"]: - return this.getRecruitmentTime(); - case ActionTypes["Diplomacy"]: - case ActionTypes["Hyperbolic Regeneration Chamber"]: - return 60; - default: - workerScript.log("bladeburner.getActionTime", errorLogText); - return -1; - } -} - -Bladeburner.prototype.getActionEstimatedSuccessChanceNetscriptFn = function(type, name, workerScript) { - const errorLogText = `Invalid action: type='${type}' name='${name}'` - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - workerScript.log("bladeburner.getActionEstimatedSuccessChance", errorLogText); - return -1; - } - - const actionObj = this.getActionObject(actionId); - if (actionObj == null) { - workerScript.log("bladeburner.getActionEstimatedSuccessChance", errorLogText); - return -1; - } - - switch (actionId.type) { - case ActionTypes["Contract"]: - case ActionTypes["Operation"]: - case ActionTypes["BlackOp"]: - case ActionTypes["BlackOperation"]: - return actionObj.getSuccessChance(this, {est:true}); - case ActionTypes["Training"]: - case ActionTypes["Field Analysis"]: - case ActionTypes["FieldAnalysis"]: - return 1; - case ActionTypes["Recruitment"]: - return this.getRecruitmentSuccessChance(); - default: - workerScript.log("bladeburner.getActionEstimatedSuccessChance", errorLogText); - return -1; - } -} - -Bladeburner.prototype.getActionCountRemainingNetscriptFn = function(type, name, workerScript) { - 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["Field Analysis"]: - case ActionTypes["FieldAnalysis"]: - return Infinity; - default: - workerScript.log("bladeburner.getActionCountRemaining", errorLogText); - return -1; - } -} - -Bladeburner.prototype.getSkillLevelNetscriptFn = function(skillName, workerScript) { - if (skillName === "" || !Skills.hasOwnProperty(skillName)) { - workerScript.log("bladeburner.getSkillLevel", `Invalid skill: '${skillName}'`); - return -1; - } - - if (this.skills[skillName] == null) { - return 0; - } else { - return this.skills[skillName]; - } -} - -Bladeburner.prototype.getSkillUpgradeCostNetscriptFn = function(skillName, workerScript) { - if (skillName === "" || !Skills.hasOwnProperty(skillName)) { - workerScript.log("bladeburner.getSkillUpgradeCost", `Invalid skill: '${skillName}'`); - return -1; - } - - const skill = Skills[skillName]; - if (this.skills[skillName] == null) { - return skill.calculateCost(0); - } else { - return skill.calculateCost(this.skills[skillName]); - } -} - -Bladeburner.prototype.upgradeSkillNetscriptFn = function(skillName, workerScript) { - const errorLogText = `Invalid skill: '${skillName}'`; - if (!Skills.hasOwnProperty(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); - - if(skill.maxLvl && currentLevel >= skill.maxLvl) { - workerScript.log("bladeburner.upgradeSkill", `Skill '${skillName}' is already maxed.`); - return false; - } - - 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})`); - return false; - } - - this.skillPoints -= cost; - this.upgradeSkill(skill); - if (routing.isOn(Page.Bladeburner) && DomElems.currentTab.toLowerCase() === "skills") { - this.createActionAndSkillsContent(); - } - workerScript.log("bladeburner.upgradeSkill", `'${skillName}' upgraded to level ${this.skills[skillName]}`); - return true; -} - -Bladeburner.prototype.getTeamSizeNetscriptFn = function(type, name, workerScript) { - 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; - } -} - -Bladeburner.prototype.setTeamSizeNetscriptFn = function(type, name, size, workerScript) { - 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; -} - -Bladeburner.prototype.joinBladeburnerFactionNetscriptFn = function(workerScript) { - var bladeburnerFac = Factions["Bladeburners"]; - if (bladeburnerFac.isMember) { - return true; - } else if (this.rank >= BladeburnerConstants.RankNeededForFaction) { - joinFaction(bladeburnerFac); - workerScript.log("bladeburner.joinBladeburnerFaction", "Joined Bladeburners faction."); - if (routing.isOn(Page.Bladeburner)) { - removeChildrenFromElement(DomElems.overviewDiv); - this.createOverviewContent(); - } - return true; - } else { - workerScript.log("bladeburner.joinBladeburnerFaction", `You do not have the required rank (${this.rank}/${BladeburnerConstants.RankNeededForFaction}).`); - return false; - } -} - -Bladeburner.prototype.toJSON = function() { - return Generic_toJSON("Bladeburner", this); -} -Bladeburner.fromJSON = function(value) { - return Generic_fromJSON(Bladeburner, value.data); -} -Reviver.constructors.Bladeburner = Bladeburner; - -export { Bladeburner }; diff --git a/src/Bladeburner/ActionIdentifier.ts b/src/Bladeburner/ActionIdentifier.ts new file mode 100644 index 000000000..4d520d833 --- /dev/null +++ b/src/Bladeburner/ActionIdentifier.ts @@ -0,0 +1,28 @@ +import { IActionIdentifier } from "./IActionIdentifier"; +import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; + +interface IParams { + name?: string; + type?: number; +} + +export class ActionIdentifier implements IActionIdentifier { + name: string = ""; + type: number = -1; + + constructor(params: IParams = {}) { + if (params.name) this.name = params.name; + if (params.type) this.type = params.type; + } + + toJSON(): any { + return Generic_toJSON("ActionIdentifier", this); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static fromJSON(value: any): ActionIdentifier { + return Generic_fromJSON(ActionIdentifier, value.data); + } +} + +Reviver.constructors.ActionIdentifier = ActionIdentifier; diff --git a/src/Bladeburner/Bladeburner.ts b/src/Bladeburner/Bladeburner.ts new file mode 100644 index 000000000..f736388d6 --- /dev/null +++ b/src/Bladeburner/Bladeburner.ts @@ -0,0 +1,1986 @@ +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 { BlackOperations } from "./BlackOperations"; +import { BlackOperation } from "./BlackOperation"; +import { Operation } from "./Operation"; +import { Contract } from "./Contract"; +import { GeneralActions } from "./GeneralActions"; +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 { ConsoleHelpText } from "./data/Help"; +import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { BladeburnerConstants } from "./data/Constants"; +import { numeralWrapper } from "../ui/numeralFormat"; +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { addOffset } from "../../utils/helpers/addOffset"; +import { Faction } from "../Faction/Faction"; +import { Factions, factionExists } from "../Faction/Factions"; +import { calculateHospitalizationCost } from "../Hospital/Hospital"; +import { hackWorldDaemon, redPillFlag } from "../RedPill"; +import { dialogBoxCreate } from "../../utils/DialogBox"; +import { Settings } from "../Settings/Settings"; +import { Augmentations } from "../Augmentation/Augmentations"; +import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; +import { getTimestamp } from "../../utils/helpers/getTimestamp"; +import { joinFaction } from "../Faction/FactionHelpers"; +import { WorkerScript } from "../Netscript/WorkerScript"; + +export class Bladeburner implements IBladeburner { + numHosp: number = 0; + moneyLost: number = 0; + rank: number = 0; + maxRank: number = 0; + + skillPoints: number = 0; + totalSkillPoints: number = 0; + + teamSize: number = 0; + teamLost: number = 0; + hpLost: number = 0; + + storedCycles: number = 0; + + randomEventCounter: number = getRandomInt(240, 600); + + actionTimeToComplete: number = 0; + actionTimeCurrent: number = 0; + actionTimeOverflow: number = 0; + + action: IActionIdentifier = new ActionIdentifier({type: ActionTypes["Idle"]}); + + cities: any = {}; + city: string = BladeburnerConstants.CityNames[2]; + skills: any = {}; + skillMultipliers: any = {}; + staminaBonus: number = 0; + maxStamina: number = 0; + stamina: number = 0; + contracts: any = {}; + operations: any = {}; + blackops: any = {}; + logging: any = { + general:true, + contracts:true, + ops:true, + blackops:true, + events:true, + }; + automateEnabled: boolean = false; + automateActionHigh: IActionIdentifier = new ActionIdentifier({type: ActionTypes["Idle"]}); + automateThreshHigh: number = 0; + automateActionLow: IActionIdentifier = new ActionIdentifier({type: ActionTypes["Idle"]}); + automateThreshLow: number = 0; + consoleHistory: string[] = []; + consoleLogs: string[] = [ + "Bladeburner Console", + "Type 'help' to see console commands", + ]; + + constructor(player?: IPlayer) { + for (var i = 0; i < BladeburnerConstants.CityNames.length; ++i) { + this.cities[BladeburnerConstants.CityNames[i]] = new City(BladeburnerConstants.CityNames[i]); + } + + this.updateSkillMultipliers(); // Calls resetSkillMultipliers() + + // Max Stamina is based on stats and Bladeburner-specific bonuses + if(player) + this.calculateMaxStamina(player); + this.stamina = this.maxStamina; + this.create(); + } + + getCurrentCity(): City { + const city = this.cities[this.city]; + if (!(city instanceof City)) { + throw new Error("Bladeburner.getCurrentCity() did not properly return a City object"); + } + return city; + } + + calculateStaminaPenalty(): number { + return Math.min(1, this.stamina / (0.5 * this.maxStamina)); + } + + startAction(player: IPlayer, actionId: IActionIdentifier): 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); + } catch(e) { + 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().commsEst === 0) {return this.resetAction();} + this.actionTimeToComplete = action.getActionTime(this); + } catch(e) { + exceptionAlert(e); + } + break; + } + case ActionTypes["BlackOp"]: + case ActionTypes["BlackOperation"]: { + try { + // Safety measure - don't repeat BlackOps that are already done + if (this.blackops[actionId.name] != null) { + this.resetAction(); + this.log("Error: Tried to start a Black Operation that had already been completed"); + break; + } + + const action = this.getActionObject(actionId); + if (action == null) { + throw new Error("Failed to get BlackOperation object for: " + actionId.name); + } + this.actionTimeToComplete = action.getActionTime(this); + } catch(e) { + 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"]: + this.actionTimeToComplete = 60; + break; + default: + throw new Error("Invalid Action Type in startAction(Bladeburner,player, ): " + actionId.type); + break; + } + } + + 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; + } + if (isNaN(this.skills[skillName]) || this.skills[skillName] < 0) { + throw new Error("Level of Skill " + skillName + " is invalid: " + this.skills[skillName]); + } + 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]); + } + } catch(e) { + exceptionAlert(e); + } + } + + postToConsole(input: string, saveToLogs: boolean = 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}); + } + + clearConsole(): void { + this.consoleLogs.length = 0; + } + + prestige(): void { + this.resetAction(); + const bladeburnerFac = Factions["Bladeburners"]; + if (this.rank >= BladeburnerConstants.RankNeededForFaction) { + joinFaction(bladeburnerFac); + } + } + + storeCycles(numCycles: number = 0): void { + this.storedCycles += numCycles; + } + + + // working on + getActionIdFromTypeAndName(type: string = "", name: string = ""): IActionIdentifier | 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 (this.contracts.hasOwnProperty(name)) { + action.name = name; + return action; + } else { + return null; + } + break; + case "operation": + case "operations": + case "op": + case "ops": + action.type = ActionTypes["Operation"]; + if (this.operations.hasOwnProperty(name)) { + action.name = name; + return action; + } else { + return null; + } + break; + 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; + } else { + return null; + } + break; + 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; + default: + return null; + } + return action; + } + + return null; + } + + executeStartConsoleCommand(player: IPlayer, 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(player, 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(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]); + 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) { + let skill = Skills[skillNames[i]]; + let level = 0; + if (this.skills[skill.name] != null) {level = this.skills[skill.name];} + this.postToConsole(skill.name + ": Level " + formatNumber(level, 0)); + } + this.postToConsole(" "); + this.postToConsole("Effects: "); + const multKeys = Object.keys(this.skillMultipliers); + for (let i = 0; i < multKeys.length; ++i) { + let mult = this.skillMultipliers[multKeys[i]]; + if (mult && mult !== 1) { + mult = formatNumber(mult, 3); + switch(multKeys[i]) { + case "successChanceAll": + this.postToConsole("Total Success Chance: x" + mult); + break; + case "successChanceStealth": + this.postToConsole("Stealth Success Chance: x" + mult); + break; + case "successChanceKill": + this.postToConsole("Retirement Success Chance: x" + mult); + break; + case "successChanceContract": + this.postToConsole("Contract Success Chance: x" + mult); + break; + case "successChanceOperation": + this.postToConsole("Operation Success Chance: x" + mult); + break; + case "successChanceEstimate": + this.postToConsole("Synthoid Data Estimate: x" + mult); + break; + case "actionTime": + this.postToConsole("Action Time: x" + mult); + break; + case "effHack": + this.postToConsole("Hacking Skill: x" + mult); + break; + case "effStr": + this.postToConsole("Strength: x" + mult); + break; + case "effDef": + this.postToConsole("Defense: x" + mult); + break; + case "effDex": + this.postToConsole("Dexterity: x" + mult); + break; + case "effAgi": + this.postToConsole("Agility: x" + mult); + break; + case "effCha": + this.postToConsole("Charisma: x" + mult); + break; + case "effInt": + this.postToConsole("Intelligence: x" + mult); + break; + case "stamina": + this.postToConsole("Stamina: x" + mult); + 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 == null || !(skill instanceof Skill)) { + this.postToConsole("Invalid skill name (Note that it is case-sensitive): " + skillName); + } + 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 (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 " + formatNumber(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")); + if (this.automateEnabled) { + this.postToConsole("When your stamina drops to " + 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 + "."); + } + + } 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; + } + + // Set variables + if (args.length === 4) { + const variable = args[1]; + 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 === '"') { // Double quotes + const endQuote = command.indexOf('"', i+1); + if (endQuote !== -1 && (endQuote === command.length-1 || command.charAt(endQuote+1) === " ")) { + 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); + if (endQuote !== -1 && (endQuote === command.length-1 || command.charAt(endQuote+1) === " ")) { + 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 === " ") { + args.push(command.substr(start, i-start)); + start = i+1; + } + ++i; + } + if (start !== i) {args.push(command.substr(start, i-start));} + return args; + } + + executeConsoleCommand(player: IPlayer, command: string) { + 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; + } + } + + 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)) { + 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(); + + // Choose random source/destination city for events + const sourceCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; + const sourceCity = this.cities[sourceCityName]; + if (!(sourceCity instanceof City)) { + throw new Error("sourceCity was not a City object in Bladeburner.randomEvent()"); + } + + let destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; + while (destCityName === sourceCityName) { + destCityName = BladeburnerConstants.CityNames[getRandomInt(0, 5)]; + } + const destCity = this.cities[destCityName]; + + if (!(sourceCity instanceof City) || !(destCity instanceof City)) { + throw new Error("sourceCity/destCity was not a City object in Bladeburner.randomEvent()"); + } + + 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) { + 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 (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 (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 (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 + } + + /** + * Process stat gains from Contracts, Operations, and Black Operations + * @param action(Action obj) - Derived action class + * @param success(bool) - Whether action was successful + */ + gainActionStats(player: IPlayer, action: IAction, success: boolean): void { + 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; + + const unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult; + const unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult; + const skillMult = this.skillMultipliers.expGain; + player.gainHackingExp(unweightedGain * action.weights.hack * player.hacking_exp_mult * skillMult); + player.gainStrengthExp(unweightedGain * action.weights.str * player.strength_exp_mult * skillMult); + player.gainDefenseExp(unweightedGain * action.weights.def * player.defense_exp_mult * skillMult); + player.gainDexterityExp(unweightedGain * action.weights.dex * player.dexterity_exp_mult * skillMult); + player.gainAgilityExp(unweightedGain * action.weights.agi * player.agility_exp_mult * skillMult); + player.gainCharismaExp(unweightedGain * action.weights.cha * player.charisma_exp_mult * skillMult); + let intExp = unweightedIntGain * action.weights.int * skillMult; + if (intExp > 1) { + intExp = Math.pow(intExp, 0.8); + } + player.gainIntelligenceExp(intExp); + } + + getDiplomacyEffectiveness(player: IPlayer): 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(player.charisma, CharismaExponentialFactor) + player.charisma / CharismaLinearFactor; + return (100 - charismaEff) / 100; + } + + getRecruitmentSuccessChance(player: IPlayer): number { + return Math.pow(player.charisma, 0.45) / (this.teamSize + 1); + } + + getRecruitmentTime(player: IPlayer): number { + const effCharisma = player.charisma * this.skillMultipliers.effCha; + const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90; + return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor)); + } + + 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 in this.skills) { + 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]; + 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; + this.teamLost += losses; + if (this.logging.ops && losses > 0) { + this.log("Lost " + formatNumber(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); + if (Math.random() < (0.02 * this.skillMultipliers.successChanceEstimate)) { + city.improveCommunityEstimate(1); + } + } else { + this.triggerPotentialMigration(this.city, 0.1); + } + break; + case "Undercover Operation": + if (success) { + city.improvePopulationEstimateByPercentage(0.8 * this.skillMultipliers.successChanceEstimate); + if (Math.random() < (0.02 * this.skillMultipliers.successChanceEstimate)) { + city.improveCommunityEstimate(1); + } + } 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; + --city.commsEst; + } 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: 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"]; + default: + return null; + } + } + + completeContract(success: boolean): void { + if (this.action.type !== ActionTypes.Contract) { + throw new Error("completeContract() called even though current action is not a Contract"); + } + var city = this.getCurrentCity(); + if (success) { + switch (this.action.name) { + 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: " + this.action.name); + } + } + } + + completeAction(player: IPlayer): void { + switch (this.action.type) { + case ActionTypes["Contract"]: + case ActionTypes["Operation"]: { + try { + const isOperation = (this.action.type === ActionTypes["Operation"]); + const action = this.getActionObject(this.action); + if (action == null) { + throw new Error("Failed to get Contract/Operation Object for: " + this.action.name); + } + const difficulty = action.getDifficulty(); + const difficultyMultiplier = Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + difficulty / BladeburnerConstants.DiffMultLinearFactor; + const rewardMultiplier = Math.pow(action.rewardFac, action.level-1); + + // 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)) { + this.gainActionStats(player, action, true); + ++action.successes; + --action.count; + + // Earn money for contracts + let moneyGain = 0; + if (!isOperation) { + moneyGain = BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.skillMultipliers.money; + player.gainMoney(moneyGain); + player.recordMoneySource(moneyGain, "bladeburner"); + } + + if (isOperation) { + action.setMaxLevel(BladeburnerConstants.OperationSuccessesPerLevel); + } else { + action.setMaxLevel(BladeburnerConstants.ContractSuccessesPerLevel); + } + if (action.rankGain) { + const gain = addOffset(action.rankGain * rewardMultiplier * BitNodeMultipliers.BladeburnerRank, 10); + this.changeRank(player, gain); + if (isOperation && this.logging.ops) { + this.log(action.name + " successfully completed! Gained " + formatNumber(gain, 3) + " rank"); + } else if (!isOperation && this.logging.contracts) { + this.log(action.name + " contract successfully completed! Gained " + formatNumber(gain, 3) + " rank and " + numeralWrapper.formatMoney(moneyGain)); + } + } + isOperation ? this.completeOperation(true) : this.completeContract(true); + } else { + this.gainActionStats(player, action, false); + ++action.failures; + let loss = 0, damage = 0; + if (action.rankLoss) { + loss = addOffset(action.rankLoss * rewardMultiplier, 10); + this.changeRank(player, -1 * loss); + } + if (action.hpLoss) { + damage = action.hpLoss * difficultyMultiplier; + damage = Math.ceil(addOffset(damage, 10)); + this.hpLost += damage; + const cost = calculateHospitalizationCost(player, damage); + if (player.takeDamage(damage)) { + ++this.numHosp; + this.moneyLost += cost; + } + } + let logLossText = ""; + if (loss > 0) {logLossText += "Lost " + formatNumber(loss, 3) + " rank. ";} + if (damage > 0) {logLossText += "Took " + formatNumber(damage, 0) + " damage.";} + if (isOperation && this.logging.ops) { + this.log(action.name + " failed! " + logLossText); + } else if (!isOperation && this.logging.contracts) { + this.log(action.name + " contract failed! " + logLossText); + } + isOperation ? this.completeOperation(false) : this.completeContract(false); + } + if (action.autoLevel) {action.level = action.maxLevel;} // Autolevel + this.startAction(player, this.action); // Repeat action + } catch(e) { + exceptionAlert(e); + } + break; + } + case ActionTypes["BlackOp"]: + case ActionTypes["BlackOperation"]: { + try { + const action = this.getActionObject(this.action); + if (action == null || !(action instanceof BlackOperation)) { + throw new Error("Failed to get BlackOperation Object for: " + this.action.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)) { + this.gainActionStats(player, action, true); + action.count = 0; + this.blackops[action.name] = true; + let rankGain = 0; + if (action.rankGain) { + rankGain = addOffset(action.rankGain * BitNodeMultipliers.BladeburnerRank, 10); + this.changeRank(player, rankGain); + } + teamLossMax = Math.ceil(teamCount/2); + + // Operation Daedalus + if (action.name === "Operation Daedalus") { + this.resetAction(); + return hackWorldDaemon(player.bitNodeN); + } + + if (this.logging.blackops) { + this.log(action.name + " successful! Gained " + formatNumber(rankGain, 1) + " rank"); + } + } else { + this.gainActionStats(player, action, false); + let rankLoss = 0; + let damage = 0; + if (action.rankLoss) { + rankLoss = addOffset(action.rankLoss, 10); + this.changeRank(player, -1 * rankLoss); + } + if (action.hpLoss) { + damage = action.hpLoss * difficultyMultiplier; + damage = Math.ceil(addOffset(damage, 10)); + const cost = calculateHospitalizationCost(player, damage); + if (player.takeDamage(damage)) { + ++this.numHosp; + this.moneyLost += cost; + } + } + teamLossMax = Math.floor(teamCount); + + if (this.logging.blackops) { + this.log(action.name + " failed! Lost " + formatNumber(rankLoss, 1) + " rank and took " + formatNumber(damage, 0) + " damage"); + } + } + + this.resetAction(); // Stop regardless of success or fail + + // Calculate team lossses + if (teamCount >= 1) { + const losses = getRandomInt(1, teamLossMax); + this.teamSize -= losses; + this.teamLost += losses; + if (this.logging.blackops) { + this.log("You lost " + formatNumber(losses, 0) + " team members during " + action.name); + } + } + } catch(e) { + exceptionAlert(e); + } + break; + } + case ActionTypes["Training"]: { + this.stamina -= (0.5 * BladeburnerConstants.BaseStaminaLoss); + const strExpGain = 30 * player.strength_exp_mult, + defExpGain = 30 * player.defense_exp_mult, + dexExpGain = 30 * player.dexterity_exp_mult, + agiExpGain = 30 * player.agility_exp_mult, + staminaGain = 0.04 * this.skillMultipliers.stamina; + player.gainStrengthExp(strExpGain); + player.gainDefenseExp(defExpGain); + player.gainDexterityExp(dexExpGain); + player.gainAgilityExp(agiExpGain); + this.staminaBonus += (staminaGain); + if (this.logging.general) { + this.log("Training completed. Gained: " + + 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"); + } + this.startAction(player, this.action); // Repeat action + 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(player.hacking_skill, 0.3) + + 0.04 * Math.pow(player.intelligence, 0.9) + + 0.02 * Math.pow(player.charisma, 0.3); + eff *= player.bladeburner_analysis_mult; + if (isNaN(eff) || eff < 0) { + throw new Error("Field Analysis Effectiveness calculated to be NaN or negative"); + } + const hackingExpGain = 20 * player.hacking_exp_mult, + charismaExpGain = 20 * player.charisma_exp_mult; + player.gainHackingExp(hackingExpGain); + player.gainIntelligenceExp(BladeburnerConstants.BaseIntGain); + player.gainCharismaExp(charismaExpGain); + this.changeRank(player, 0.1 * BitNodeMultipliers.BladeburnerRank); + this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate); + if (this.logging.general) { + this.log("Field analysis completed. Gained 0.1 rank, " + formatNumber(hackingExpGain, 1) + " hacking exp, and " + formatNumber(charismaExpGain, 1) + " charisma exp"); + } + this.startAction(player, this.action); // Repeat action + break; + } + case ActionTypes["Recruitment"]: { + const successChance = this.getRecruitmentSuccessChance(player); + if (Math.random() < successChance) { + const expGain = 2 * BladeburnerConstants.BaseStatGain * this.actionTimeToComplete; + player.gainCharismaExp(expGain); + ++this.teamSize; + if (this.logging.general) { + this.log("Successfully recruited a team member! Gained " + formatNumber(expGain, 1) + " charisma exp"); + } + } else { + const expGain = BladeburnerConstants.BaseStatGain * this.actionTimeToComplete; + player.gainCharismaExp(expGain); + if (this.logging.general) { + this.log("Failed to recruit a team member. Gained " + formatNumber(expGain, 1) + " charisma exp"); + } + } + this.startAction(player, this.action); // Repeat action + break; + } + case ActionTypes["Diplomacy"]: { + let eff = this.getDiplomacyEffectiveness(player); + this.getCurrentCity().chaos *= eff; + if (this.getCurrentCity().chaos < 0) { this.getCurrentCity().chaos = 0; } + if (this.logging.general) { + this.log(`Diplomacy completed. Chaos levels in the current city fell by ${numeralWrapper.formatPercentage(1 - eff)}`); + } + this.startAction(player, this.action); // Repeat Action + break; + } + case ActionTypes["Hyperbolic Regeneration Chamber"]: { + player.regenerateHp(BladeburnerConstants.HrcHpGain); + + const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100); + this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain); + this.startAction(player, this.action); + if (this.logging.general) { + this.log(`Rested in Hyperbolic Regeneration Chamber. Restored ${BladeburnerConstants.HrcHpGain} HP and gained ${numeralWrapper.formatStamina(staminaGain)} stamina`); + } + break; + } + default: + console.error(`Bladeburner.completeAction() called for invalid action: ${this.action.type}`); + break; + } + } + + changeRank(player: IPlayer, 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 = "Bladeburners"; + if (factionExists(bladeburnersFactionName)) { + const bladeburnerFac = Factions[bladeburnersFactionName]; + if (!(bladeburnerFac instanceof Faction)) { + throw new Error("Could not properly get Bladeburner Faction object in Bladeburner UI Overview Faction button"); + } + if (bladeburnerFac.isMember) { + const favorBonus = 1 + (bladeburnerFac.favor / 100); + bladeburnerFac.playerReputation += (BladeburnerConstants.RankToFactionRepFactor * change * player.faction_rep_mult * 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(player: IPlayer, 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 instanceof ActionIdentifier)) { + throw new Error("Bladeburner.action is not an ActionIdentifier Object"); + } + + // 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) { + this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete; + return this.completeAction(player); + } + } + + calculateStaminaGainPerSecond(player: IPlayer): number { + const effAgility = player.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.bladeburner_stamina_gain_mult); + } + + calculateMaxStamina(player: IPlayer) { + const effAgility = player.agility * this.skillMultipliers.effAgi; + let 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)) {throw new Error("Max Stamina calculated to be NaN in Bladeburner.calculateMaxStamina()");} + } + + create(): void { + this.contracts["Tracking"] = new Contract({ + name:"Tracking", + desc:"Identify and locate Synthoids. This contract involves reconnaissance " + + "and information-gathering ONLY. Do NOT engage. Stealth is of the utmost importance.

" + + "Successfully completing Tracking contracts will slightly improve your Synthoid population estimate for " + + "whatever city you are currently in.", + baseDifficulty:125,difficultyFac:1.02,rewardFac:1.041, + rankGain:0.3, hpLoss:0.5, + count:getRandomInt(25, 150), countGrowth:getRandomInt(5, 75)/10, + 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", + desc:"Hunt down and capture fugitive Synthoids. These Synthoids are wanted alive.

" + + "Successfully completing a Bounty Hunter contract will lower the population in your " + + "current city, and will also increase its chaos level.", + baseDifficulty:250, difficultyFac:1.04,rewardFac:1.085, + rankGain:0.9, hpLoss:1, + count:getRandomInt(5, 150), countGrowth:getRandomInt(5, 75)/10, + 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", + desc:"Hunt down and retire (kill) rogue Synthoids.

" + + "Successfully completing a Retirement contract will lower the population in your current " + + "city, and will also increase its chaos level.", + baseDifficulty:200, difficultyFac:1.03, rewardFac:1.065, + rankGain:0.6, hpLoss:1, + count:getRandomInt(5, 150), countGrowth:getRandomInt(5, 75)/10, + 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", + desc:"As a field agent, investigate and identify Synthoid " + + "populations, movements, and operations.

Successful " + + "Investigation ops will increase the accuracy of your " + + "synthoid data.

" + + "You will NOT lose HP from failed Investigation ops.", + baseDifficulty:400, difficultyFac:1.03,rewardFac:1.07,reqdRank:25, + rankGain:2.2, rankLoss:0.2, + count:getRandomInt(1, 100), countGrowth:getRandomInt(10, 40)/10, + 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", + desc:"Conduct undercover operations to identify hidden " + + "and underground Synthoid communities and organizations.

" + + "Successful Undercover ops will increase the accuracy of your synthoid " + + "data.", + baseDifficulty:500, difficultyFac:1.04, rewardFac:1.09, reqdRank:100, + rankGain:4.4, rankLoss:0.4, hpLoss:2, + count:getRandomInt(1, 100), countGrowth:getRandomInt(10, 40)/10, + 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", + desc:"Conduct a sting operation to bait and capture particularly " + + "notorious Synthoid criminals.", + baseDifficulty:650, difficultyFac:1.04, rewardFac:1.095, reqdRank:500, + rankGain:5.5, rankLoss:0.5, hpLoss:2.5, + count:getRandomInt(1, 150), countGrowth:getRandomInt(3, 40)/10, + 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", + desc:"Lead an assault on a known Synthoid community. Note that " + + "there must be an existing Synthoid community in your current city " + + "in order for this Operation to be successful.", + baseDifficulty:800, difficultyFac:1.045, rewardFac:1.1, reqdRank:3000, + rankGain:55,rankLoss:2.5,hpLoss:50, + count:getRandomInt(1, 150), countGrowth:getRandomInt(2, 40)/10, + 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", + desc:"Lead a covert operation to retire Synthoids. The " + + "objective is to complete the task without " + + "drawing any attention. Stealth and discretion are key.", + baseDifficulty:1000, difficultyFac:1.05, rewardFac:1.11, reqdRank:20e3, + rankGain:22, rankLoss:2, hpLoss:10, + count:getRandomInt(1, 150), countGrowth:getRandomInt(1, 20)/10, + 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", + desc:"Assassinate Synthoids that have been identified as " + + "important, high-profile social and political leaders " + + "in the Synthoid communities.", + baseDifficulty:1500, difficultyFac:1.06, rewardFac:1.14, reqdRank:50e3, + rankGain:44, rankLoss:4, hpLoss:5, + count:getRandomInt(1, 150), countGrowth:getRandomInt(1, 20)/10, + 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(player: IPlayer): void { + // Edge case condition...if Operation Daedalus is complete trigger the BitNode + if (redPillFlag === false && this.blackops.hasOwnProperty("Operation Daedalus")) { + return hackWorldDaemon(player.bitNodeN); + } + + // If the Player starts doing some other actions, set action to idle and alert + if (Augmentations[AugmentationNames.BladesSimulacrum].owned === false && player.isWorking) { + if (this.action.type !== ActionTypes["Idle"]) { + let msg = "Your Bladeburner action was cancelled because you started doing something else."; + if (this.automateEnabled) { + msg += `

Your 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(player); + this.stamina += (this.calculateStaminaGainPerSecond(player) * seconds); + this.stamina = Math.min(this.maxStamina, this.stamina); + + // Count increase for contracts/operations + for (const contract of (Object.values(this.contracts) as Contract[])) { + contract.count += (seconds * contract.countGrowth/BladeburnerConstants.ActionCountGrowthPeriod); + } + for (const op of (Object.values(this.operations) as Operation[])) { + op.count += (seconds * op.countGrowth/BladeburnerConstants.ActionCountGrowthPeriod); + } + + // Chaos goes down very slowly + for (const cityName of BladeburnerConstants.CityNames) { + const city = this.cities[cityName]; + if (!(city instanceof City)) {throw new Error("Invalid City object 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(player, 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(player, 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(player, this.action); + } + } + } + } + } + + 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; + } + } + 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(player: IPlayer, type: string, name: string, workerScript: WorkerScript) { + 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"]) { + // Can't start a BlackOp if you don't have the required rank + const action = this.getActionObject(actionId); + if(action == null) throw new Error('Action not found ${actionId.type}, ${actionId.name}'); + if(!(action instanceof BlackOperation)) throw new Error(`Action should be BlackOperation but isn't`); + const blackOp = (action as BlackOperation); + if (action.reqdRank > this.rank) { + workerScript.log("bladeburner.startAction", `Insufficient rank to start Black Op '${actionId.name}'.`); + return false; + } + + // Can't start a BlackOp if its already been done + if (this.blackops[actionId.name] != null) { + workerScript.log("bladeburner.startAction", `Black Op ${actionId.name} has already been completed.`); + return false; + } + + // Can't start a BlackOp if you haven't done the one before it + var blackops = []; + for (const nm in 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 + }); + + let i = blackops.indexOf(actionId.name); + if (i === -1) { + workerScript.log("bladeburner.startAction", `Invalid Black Op: '${name}'`); + return false; + } + + if (i > 0 && this.blackops[blackops[i-1]] == null) { + workerScript.log("bladeburner.startAction", `Preceding Black Op must be completed before starting '${actionId.name}'.`); + return false; + } + } + + try { + this.startAction(player, actionId); + workerScript.log("bladeburner.startAction", `Starting bladeburner action with type '${type}' and name ${name}"`); + return true; + } catch(e) { + this.resetAction(); + workerScript.log("bladeburner.startAction", errorLogText); + return false; + } + } + getActionTimeNetscriptFn(player: IPlayer, 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.getActionTime", errorLogText); + return -1; + } + + const actionObj = this.getActionObject(actionId); + if (actionObj == null) { + workerScript.log("bladeburner.getActionTime", errorLogText); + return -1; + } + + switch (actionId.type) { + case ActionTypes["Contract"]: + case ActionTypes["Operation"]: + case ActionTypes["BlackOp"]: + case ActionTypes["BlackOperation"]: + return actionObj.getActionTime(this); + case ActionTypes["Training"]: + case ActionTypes["Field Analysis"]: + case ActionTypes["FieldAnalysis"]: + return 30; + case ActionTypes["Recruitment"]: + return this.getRecruitmentTime(player); + case ActionTypes["Diplomacy"]: + case ActionTypes["Hyperbolic Regeneration Chamber"]: + return 60; + default: + workerScript.log("bladeburner.getActionTime", errorLogText); + return -1; + } + } + getActionEstimatedSuccessChanceNetscriptFn(player: IPlayer, 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.getActionEstimatedSuccessChance", errorLogText); + return -1; + } + + const actionObj = this.getActionObject(actionId); + if (actionObj == null) { + workerScript.log("bladeburner.getActionEstimatedSuccessChance", errorLogText); + return -1; + } + + switch (actionId.type) { + case ActionTypes["Contract"]: + case ActionTypes["Operation"]: + case ActionTypes["BlackOp"]: + case ActionTypes["BlackOperation"]: + return actionObj.getSuccessChance(this, {est:true}); + case ActionTypes["Training"]: + case ActionTypes["Field Analysis"]: + case ActionTypes["FieldAnalysis"]: + return 1; + case ActionTypes["Recruitment"]: + return this.getRecruitmentSuccessChance(player); + default: + workerScript.log("bladeburner.getActionEstimatedSuccessChance", errorLogText); + return -1; + } + } + getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript) { + 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["Field Analysis"]: + case ActionTypes["FieldAnalysis"]: + return Infinity; + default: + workerScript.log("bladeburner.getActionCountRemaining", errorLogText); + return -1; + } + } + getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript) { + if (skillName === "" || !Skills.hasOwnProperty(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, workerScript: WorkerScript) { + if (skillName === "" || !Skills.hasOwnProperty(skillName)) { + workerScript.log("bladeburner.getSkillUpgradeCost", `Invalid skill: '${skillName}'`); + return -1; + } + + const skill = Skills[skillName]; + if (this.skills[skillName] == null) { + return skill.calculateCost(0); + } else { + return skill.calculateCost(this.skills[skillName]); + } + } + upgradeSkillNetscriptFn(skillName: string, workerScript: WorkerScript) { + const errorLogText = `Invalid skill: '${skillName}'`; + if (!Skills.hasOwnProperty(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); + + if(skill.maxLvl && currentLevel >= skill.maxLvl) { + workerScript.log("bladeburner.upgradeSkill", `Skill '${skillName}' is already maxed.`); + return false; + } + + 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})`); + return false; + } + + this.skillPoints -= cost; + this.upgradeSkill(skill); + 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 { + var bladeburnerFac = Factions["Bladeburners"]; + if (bladeburnerFac.isMember) { + return true; + } else if (this.rank >= BladeburnerConstants.RankNeededForFaction) { + joinFaction(bladeburnerFac); + workerScript.log("bladeburner.joinBladeburnerFaction", "Joined 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(): 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); + } +} + +Reviver.constructors.Bladeburner = Bladeburner; \ No newline at end of file diff --git a/src/Bladeburner/City.ts b/src/Bladeburner/City.ts index 75f8aff24..361593c5f 100644 --- a/src/Bladeburner/City.ts +++ b/src/Bladeburner/City.ts @@ -4,14 +4,14 @@ import { getRandomInt } from "../../utils/helpers/getRandomInt"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; import { addOffset } from "../../utils/helpers/addOffset"; -export class ChangePopulationByCountParams { - estChange = 0; - estOffset = 0; +interface IChangePopulationByCountParams { + estChange: number; + estOffset: number; } -export class ChangePopulationByPercentageParams { - nonZero = false; - changeEstEqually = false; +interface IChangePopulationByPercentageParams { + nonZero: boolean; + changeEstEqually: boolean; } export class City { @@ -113,7 +113,7 @@ export class City { * estChange(int): How much the estimate should change by * estOffset(int): Add offset to estimate (offset by percentage) */ - changePopulationByCount(n: number, params: ChangePopulationByCountParams=new ChangePopulationByCountParams()): void { + changePopulationByCount(n: number, params: IChangePopulationByCountParams = {estChange: 0, estOffset: 0}): void { if (isNaN(n)) {throw new Error("NaN passed into City.changePopulationByCount()");} this.pop += n; if (params.estChange && !isNaN(params.estChange)) {this.popEst += params.estChange;} @@ -129,7 +129,7 @@ export class City { * changeEstEqually(bool) - Change the population estimate by an equal amount * nonZero (bool) - Set to true to ensure that population always changes by at least 1 */ - changePopulationByPercentage(p: number, params: ChangePopulationByPercentageParams=new ChangePopulationByPercentageParams()): number { + changePopulationByPercentage(p: number, params: IChangePopulationByPercentageParams={nonZero: false, changeEstEqually: false}): number { if (isNaN(p)) {throw new Error("NaN passed into City.changePopulationByPercentage()");} if (p === 0) {return 0;} let change = Math.round(this.pop * (p/100)); diff --git a/src/Bladeburner/IActionIdentifier.ts b/src/Bladeburner/IActionIdentifier.ts index 450ba774f..7be44d65c 100644 --- a/src/Bladeburner/IActionIdentifier.ts +++ b/src/Bladeburner/IActionIdentifier.ts @@ -1,4 +1,4 @@ export interface IActionIdentifier { name: string; - type: string; + type: number; } \ No newline at end of file diff --git a/src/Bladeburner/IBladeburner.ts b/src/Bladeburner/IBladeburner.ts index 7a7d9cd0e..b69c2c40a 100644 --- a/src/Bladeburner/IBladeburner.ts +++ b/src/Bladeburner/IBladeburner.ts @@ -1,20 +1,33 @@ import { IActionIdentifier } from "./IActionIdentifier"; import { City } from "./City"; +import { Skill } from "./Skill"; +import { IAction } from "./IAction"; +import { IPlayer } from "../PersonObjects/IPlayer"; +import { WorkerScript } from "../Netscript/WorkerScript"; export interface IBladeburner { numHosp: number; moneyLost: number; rank: number; maxRank: number; + skillPoints: number; totalSkillPoints: number; + teamSize: number; teamLost: number; + hpLost: number; + storedCycles: number; + randomEventCounter: number; + actionTimeToComplete: number; actionTimeCurrent: number; + actionTimeOverflow: number; + action: IActionIdentifier; + cities: any; city: string; skills: any; @@ -27,13 +40,66 @@ export interface IBladeburner { blackops: any; logging: any; automateEnabled: boolean; - automateActionHigh: number; + automateActionHigh: IActionIdentifier; automateThreshHigh: number; - automateActionLow: number; + automateActionLow: IActionIdentifier; automateThreshLow: number; consoleHistory: string[]; consoleLogs: string[]; getCurrentCity(): City; calculateStaminaPenalty(): number; + startAction(player: IPlayer, action: IActionIdentifier): void; + upgradeSkill(skill: Skill): void; + executeConsoleCommands(player: IPlayer, command: string): void; + postToConsole(input: string, saveToLogs?: boolean): void; + log(input: string): void; + resetAction(): void; + clearConsole(): void; + + prestige(): void; + storeCycles(numCycles?: number): void; + getTypeAndNameFromActionId(actionId: IActionIdentifier): {type: string, name: string}; + getContractNamesNetscriptFn(): string[]; + getOperationNamesNetscriptFn(): string[]; + getBlackOpNamesNetscriptFn(): string[]; + getGeneralActionNamesNetscriptFn(): string[]; + getSkillNamesNetscriptFn(): string[]; + startActionNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): boolean; + getActionTimeNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): number; + getActionEstimatedSuccessChanceNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): number; + getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number; + getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number; + getSkillUpgradeCostNetscriptFn(skillName: string, workerScript: WorkerScript): number; + upgradeSkillNetscriptFn(skillName: string, workerScript: WorkerScript): boolean; + getTeamSizeNetscriptFn(type: string, name: string, workerScript: WorkerScript): number; + setTeamSizeNetscriptFn(type: string, name: string, size: number, workerScript: WorkerScript): number; + joinBladeburnerFactionNetscriptFn(workerScript: WorkerScript): boolean; + getActionIdFromTypeAndName(type: string, name: string): IActionIdentifier | null; + executeStartConsoleCommand(player: IPlayer, args: string[]): void; + executeSkillConsoleCommand(args: string[]): void; + executeLogConsoleCommand(args: string[]): void; + executeHelpConsoleCommand(args: string[]): void; + executeAutomateConsoleCommand(args: string[]): void; + parseCommandArguments(command: string): string[]; + executeConsoleCommand(player: IPlayer, command: string): void; + triggerMigration(sourceCityName: string): void; + triggerPotentialMigration(sourceCityName: string, chance: number): void; + randomEvent(): void; + gainActionStats(player: IPlayer, action: IAction, success: boolean): void; + getDiplomacyEffectiveness(player: IPlayer): number; + getRecruitmentSuccessChance(player: IPlayer): number; + getRecruitmentTime(player: IPlayer): number; + resetSkillMultipliers(): void; + updateSkillMultipliers(): void; + completeOperation(success: boolean): void; + getActionObject(actionId: IActionIdentifier): IAction | null; + completeContract(success: boolean): void; + completeAction(player: IPlayer): void; + changeRank(player: IPlayer, change: number): void; + processAction(player: IPlayer, seconds: number): void; + calculateStaminaGainPerSecond(player: IPlayer): number; + calculateMaxStamina(player: IPlayer): void; + create(): void; + process(player: IPlayer): void; } \ No newline at end of file diff --git a/src/Bladeburner/Skill.ts b/src/Bladeburner/Skill.ts index deb84751e..7021d4311 100644 --- a/src/Bladeburner/Skill.ts +++ b/src/Bladeburner/Skill.ts @@ -103,5 +103,28 @@ export class Skill { calculateCost(currentLevel: number): number { return Math.floor((this.baseCost + (currentLevel * this.costInc)) * BitNodeMultipliers.BladeburnerSkillCost); } + + getMultiplier(name: string): number { + if(name === "successChanceAll") return this.successChanceAll; + if(name === "successChanceStealth") return this.successChanceStealth; + if(name === "successChanceKill") return this.successChanceKill; + if(name === "successChanceContract") return this.successChanceContract; + if(name === "successChanceOperation") return this.successChanceOperation; + if(name === "successChanceEstimate") return this.successChanceEstimate; + + if(name === "actionTime") return this.actionTime; + + if(name === "effHack") return this.effHack; + if(name === "effStr") return this.effStr; + if(name === "effDef") return this.effDef; + if(name === "effDex") return this.effDex; + if(name === "effAgi") return this.effAgi; + if(name === "effCha") return this.effCha; + + if(name === "stamina") return this.stamina; + if(name === "money") return this.money; + if(name === "expGain") return this.expGain; + return 0; + } } diff --git a/src/Bladeburner/data/ActionTypes.ts b/src/Bladeburner/data/ActionTypes.ts index f4392313c..a1252b1e4 100644 --- a/src/Bladeburner/data/ActionTypes.ts +++ b/src/Bladeburner/data/ActionTypes.ts @@ -1,5 +1,18 @@ // Action Identifier enum -export const ActionTypes = Object.freeze({ +export const ActionTypes: { + [key: string]: number; + "Idle": number; + "Contract": number; + "Operation": number; + "BlackOp": number; + "BlackOperation": number; + "Training": number; + "Recruitment": number; + "FieldAnalysis": number; + "Field Analysis": number; + "Diplomacy": number; + "Hyperbolic Regeneration Chamber": number; +} = { "Idle": 1, "Contract": 2, "Operation": 3, @@ -11,4 +24,4 @@ export const ActionTypes = Object.freeze({ "Field Analysis": 7, "Diplomacy": 8, "Hyperbolic Regeneration Chamber": 9, -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/src/Bladeburner/data/Help.ts b/src/Bladeburner/data/Help.ts index b08f86f10..b2d085681 100644 --- a/src/Bladeburner/data/Help.ts +++ b/src/Bladeburner/data/Help.ts @@ -1,4 +1,5 @@ export const ConsoleHelpText: { + [key: string]: string[]; helpList: string[]; automate: string[]; clear: string[]; diff --git a/src/Bladeburner/data/Icons.tsx b/src/Bladeburner/data/Icons.tsx new file mode 100644 index 000000000..296c1d249 --- /dev/null +++ b/src/Bladeburner/data/Icons.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +export const stealthIcon = + + + + + +export const killIcon = + + + + + \ No newline at end of file diff --git a/src/Bladeburner/ui/AllPages.tsx b/src/Bladeburner/ui/AllPages.tsx new file mode 100644 index 000000000..2ead8cf2f --- /dev/null +++ b/src/Bladeburner/ui/AllPages.tsx @@ -0,0 +1,49 @@ +import React, { useState, useEffect } from "react"; +import { GeneralActionPage } from "./GeneralActionPage"; +import { ContractPage } from "./ContractPage"; +import { OperationPage } from "./OperationPage"; +import { BlackOpPage } from "./BlackOpPage"; +import { SkillPage } from "./SkillPage"; +import { stealthIcon, killIcon } from "../data/Icons"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function AllPages(props: IProps): React.ReactElement { + const [page, setPage] = useState('General'); + const setRerender = useState(false)[1]; + + useEffect(() => { + const id = setInterval(() => setRerender(old => !old), 1000); + return () => clearInterval(id); + }, []); + + function Header(props: {name: string}): React.ReactElement { + return (setPage(props.name)} + className={page !== props.name ? + "bladeburner-nav-button" : + "bladeburner-nav-button-inactive"}> + {props.name} + ); + } + return (<> +
+
+
+
+
+
+ {page === 'General' && } + {page === 'Contracts' && } + {page === 'Operations' && } + {page === 'BlackOps' && } + {page === 'Skills' && } +
+ {stealthIcon}= This action requires stealth, {killIcon} = This action involves retirement + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/BlackOpElem.tsx b/src/Bladeburner/ui/BlackOpElem.tsx new file mode 100644 index 000000000..f1961c35a --- /dev/null +++ b/src/Bladeburner/ui/BlackOpElem.tsx @@ -0,0 +1,87 @@ +import React, { useState } from "react"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { ActionTypes } from "../data/ActionTypes"; +import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; +import { stealthIcon, killIcon } from "../data/Icons"; +import { createPopup } from "../../ui/React/createPopup"; +import { TeamSizePopup } from "./TeamSizePopup"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; + action: any; +} + +export function BlackOpElem(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + const isCompleted = (props.bladeburner.blackops[props.action.name] != null); + if(isCompleted) { + return ( +

{props.action.name} (COMPLETED)

); + } + + const isActive = props.bladeburner.action.type === ActionTypes["BlackOperation"] && props.action.name === props.bladeburner.action.name; + const estimatedSuccessChance = props.action.getSuccessChance(props.bladeburner, {est:true}); + const actionTime = props.action.getActionTime(props.bladeburner); + const hasReqdRank = props.bladeburner.rank >= props.action.reqdRank; + const computedActionTimeCurrent = Math.min(props.bladeburner.actionTimeCurrent+props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeToComplete); + + function onStart() { + props.bladeburner.action.type = ActionTypes.BlackOperation; + props.bladeburner.action.name = props.action.name; + props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + function onTeam() { + const popupId = "bladeburner-operation-set-team-size-popup"; + createPopup(popupId, TeamSizePopup, { + bladeburner: props.bladeburner, + action: props.action, + popupId: popupId, + }); + } + + return (<> +

+ {isActive ? + <>{props.action.name} (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} / {formatNumber(props.bladeburner.actionTimeToComplete, 0)}) : + <>{props.action.name} + } +

+ {isActive ? +

{createProgressBarText({progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete})}

: + <> + Start + + Set Team Size (Curr Size: {formatNumber(props.action.teamCount, 0)}) + + } +
+
+

+
+
+

+ Required Rank: {formatNumber(props.action.reqdRank, 0)} +

+
+

+ Estimated Success Chance: {formatNumber(estimatedSuccessChance*100, 1)}% {props.action.isStealth?stealthIcon:<>}{props.action.isKill?killIcon:<>} +
+ Time Required: {convertTimeMsToTimeElapsedString(actionTime*1000)} +

+ ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/BlackOpList.tsx b/src/Bladeburner/ui/BlackOpList.tsx new file mode 100644 index 000000000..7f35db9a9 --- /dev/null +++ b/src/Bladeburner/ui/BlackOpList.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { ActionTypes } from "../data/ActionTypes"; +import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; +import { stealthIcon, killIcon } from "../data/Icons"; +import { BlackOperations } from "../BlackOperations"; +import { BlackOperation } from "../BlackOperation"; +import { BlackOpElem } from "./BlackOpElem"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function BlackOpList(props: IProps): React.ReactElement { + let blackops: BlackOperation[] = []; + for (const blackopName in BlackOperations) { + if (BlackOperations.hasOwnProperty(blackopName)) { + blackops.push(BlackOperations[blackopName]); + } + } + blackops.sort(function(a, b) { + return (a.reqdRank - b.reqdRank); + }); + + blackops = blackops.filter((blackop: BlackOperation, i: number) => !(props.bladeburner.blackops[blackops[i].name] == null && + i !== 0 && + props.bladeburner.blackops[blackops[i-1].name] == null)); + + blackops = blackops.reverse(); + + return (<> + {blackops.map((blackop: BlackOperation) =>
  • + +
  • , + )} + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/BlackOpPage.tsx b/src/Bladeburner/ui/BlackOpPage.tsx new file mode 100644 index 000000000..73ead8d09 --- /dev/null +++ b/src/Bladeburner/ui/BlackOpPage.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { BlackOpList } from "./BlackOpList"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function BlackOpPage(props: IProps): React.ReactElement { + return (<> +

    + Black Operations (Black Ops) are special, one-time covert operations. + Each Black Op must be unlocked successively by completing + the one before it. +
    +
    + Your ultimate goal to climb through the ranks of Bladeburners is to complete + all of the Black Ops. +
    +
    + Like normal operations, you may use a team for Black Ops. Failing + a black op will incur heavy HP and rank losses. +

    + + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/BlackOperationsPage.tsx b/src/Bladeburner/ui/BlackOperationsPage.tsx deleted file mode 100644 index 7f73397c1..000000000 --- a/src/Bladeburner/ui/BlackOperationsPage.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { BlackOperations } from "../BlackOperations"; -/* -if (DomElems.actionsAndSkillsList == null || DomElems.actionsAndSkillsDesc == null) { - throw new Error("Bladeburner.createBlackOpsContent called with either " + - "DomElems.actionsAndSkillsList or DomElems.actionsAndSkillsDesc = null"); -} - -DomElems.actionsAndSkillsDesc.innerHTML = - "Black Operations (Black Ops) are special, one-time covert operations. " + - "Each Black Op must be unlocked successively by completing " + - "the one before it.

    " + - "Your ultimate goal to climb through the ranks of Bladeburners is to complete " + - "all of the Black Ops.

    " + - "Like normal operations, you may use a team for Black Ops. Failing " + - "a black op will incur heavy HP and rank losses."; - -// Put Black Operations in sequence of required rank -var blackops = []; -for (var blackopName in BlackOperations) { - if (BlackOperations.hasOwnProperty(blackopName)) { - blackops.push(BlackOperations[blackopName]); - } -} -blackops.sort(function(a, b) { - return (a.reqdRank - b.reqdRank); -}); - -for (var i = blackops.length-1; i >= 0 ; --i) { - if (this.blackops[[blackops[i].name]] == null && i !== 0 && this.blackops[[blackops[i-1].name]] == null) {continue;} // If this one nor the next are completed then this isn't unlocked yet. - DomElems.blackops[blackops[i].name] = createElement("div", { - class:"bladeburner-action", name:blackops[i].name - }); - DomElems.actionsAndSkillsList.appendChild(DomElems.blackops[blackops[i].name]); -} -*/ - - - -import * as React from "react"; - -export function BlackOperationsPage(): React.ReactElement { - // Put Black Operations in sequence of required rank - const blackops = []; - for (const name in BlackOperations) { - if (BlackOperations.hasOwnProperty(name)) { - blackops.push(BlackOperations[name]); - } - } - blackops.sort(function(a, b) { - return (a.reqdRank - b.reqdRank); - }); - - return (
    -

    - Black Operations (Black Ops) are special, one-time covert operations. Each Black Op must be unlocked successively by completing the one before it.

    - Your ultimate goal to climb through the ranks of Bladeburners is to complete all of the Black Ops.

    - Like normal operations, you may use a team for Black Ops. Failing a black op will incur heavy HP and rank losses.

    - {blackops.map(() =>
    -
    , - )} -
    ) -} diff --git a/src/Bladeburner/ui/Console.tsx b/src/Bladeburner/ui/Console.tsx new file mode 100644 index 000000000..b09392218 --- /dev/null +++ b/src/Bladeburner/ui/Console.tsx @@ -0,0 +1,115 @@ +import React, { useState, useRef, useEffect } from "react"; +import { IBladeburner } from "../IBladeburner"; + +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface ILineProps { + content: any; +} + +function Line(props: ILineProps): React.ReactElement { + return ( + {props.content} + ) +} + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function Console(props: IProps): React.ReactElement { + const lastRef = useRef(null); + const setRerender = useState(false)[1]; + + const [consoleHistoryIndex, setConsoleHistoryIndex] = useState(props.bladeburner.consoleHistory.length); + + // TODO: Figure out how to actually make the scrolling work correctly. + function scrollToBottom() { + if(lastRef.current) + lastRef.current.scrollTop = lastRef.current.scrollHeight; + } + + function rerender() { + setRerender(old => !old); + } + + useEffect(() => { + const id = setInterval(rerender, 1000); + const id2 = setInterval(scrollToBottom, 100); + return () => { + clearInterval(id); + clearInterval(id2); + }; + }, []); + + function handleKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) { + event.preventDefault(); + const command = event.currentTarget.value; + event.currentTarget.value = ""; + if (command.length > 0) { + props.bladeburner.postToConsole("> " + command); + props.bladeburner.executeConsoleCommands(props.player, command); + setConsoleHistoryIndex(props.bladeburner.consoleHistory.length); + rerender(); + } + } + + const consoleHistory = props.bladeburner.consoleHistory; + + if (event.keyCode === 38) { // up + let i = consoleHistoryIndex; + const len = consoleHistory.length; + if (len === 0) {return;} + if (i < 0 || i > len) { + setConsoleHistoryIndex(len); + } + + if (i !== 0) { + i = i-1; + } + setConsoleHistoryIndex(i); + const prevCommand = consoleHistory[i]; + event.currentTarget.value = prevCommand; + } + + if (event.keyCode === 40) { + const i = consoleHistoryIndex; + const len = consoleHistory.length; + + if (len == 0) {return;} + if (i < 0 || i > len) { + setConsoleHistoryIndex(len); + } + + // Latest command, put nothing + if (i == len || i == len-1) { + setConsoleHistoryIndex(len); + event.currentTarget.value = ""; + } else { + setConsoleHistoryIndex(consoleHistoryIndex+1); + const prevCommand = consoleHistory[consoleHistoryIndex+1]; + event.currentTarget.value = prevCommand; + } + } + } + + return (
    + + + {/* + TODO: optimize this. + using `i` as a key here isn't great because it'll re-render everything + everytime the console reaches max length. + */} + {props.bladeburner.consoleLogs.map((log: any, i: number) => )} + + + + +
    +
    {"> "}
    +
    +
    ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/ContractElem.tsx b/src/Bladeburner/ui/ContractElem.tsx new file mode 100644 index 000000000..d479b4299 --- /dev/null +++ b/src/Bladeburner/ui/ContractElem.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react"; +import { ActionTypes } from "../data/ActionTypes"; +import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { stealthIcon, killIcon } from "../data/Icons"; +import { BladeburnerConstants } from "../data/Constants"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; + action: any; +} + +export function ContractElem(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + const isActive = props.bladeburner.action.type === ActionTypes["Contract"] && props.action.name === props.bladeburner.action.name; + const estimatedSuccessChance = props.action.getSuccessChance(props.bladeburner, {est:true}); + const computedActionTimeCurrent = Math.min(props.bladeburner.actionTimeCurrent+props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeToComplete); + const maxLevel = (props.action.level >= props.action.maxLevel); + const actionTime = props.action.getActionTime(props.bladeburner); + const autolevelCheckboxId = `bladeburner-${props.action.name}-autolevel-checkbox`; + + function onStart() { + props.bladeburner.action.type = ActionTypes.Contract; + props.bladeburner.action.name = props.action.name; + props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + function increaseLevel() { + ++props.action.level; + if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + function decreaseLevel() { + --props.action.level; + if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + function onAutolevel(event: React.ChangeEvent) { + props.action.autoLevel = event.target.checked; + setRerender(old => !old); + } + + return (<> +

    + {isActive ? + <>{props.action.name} (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} / {formatNumber(props.bladeburner.actionTimeToComplete, 0)}) : + <>{props.action.name} + } +

    + {isActive ? +

    {createProgressBarText({progress:computedActionTimeCurrent / props.bladeburner.actionTimeToComplete})}

    : + <> + + Start + + } +
    +
    +
    +            
    +                {props.action.getSuccessesNeededForNextLevel(BladeburnerConstants.ContractSuccessesPerLevel)} successes needed for next level
    +            
    +            Level: {props.action.level} / {props.action.maxLevel}
    +        
    + + {isActive && (WARNING: changing the level will restart the Operation)} + ↑ + + + {isActive && (WARNING: changing the level will restart the Operation)} + ↓ + +
    +
    +
    +
    +

    +Estimated success chance: {formatNumber(estimatedSuccessChance*100, 1)}% {props.action.isStealth?stealthIcon:<>}${props.action.isKill?killIcon:<>}
    +Time Required: {convertTimeMsToTimeElapsedString(actionTime*1000)}
    +Contracts remaining: {Math.floor(props.action.count)}
    +Successes: {props.action.successes}
    +Failures: {props.action.failures} +
    +
    + + + ); +} + +/* + +// Autolevel Checkbox +el.appendChild(createElement("br")); +var autolevelCheckboxId = "bladeburner-" + action.name + "-autolevel-checkbox"; +el.appendChild(createElement("label", { + for:autolevelCheckboxId, innerText:"Autolevel: ",color:"white", + tooltip:"Automatically increase contract level when possible", +})); + +const checkboxInput = createElement("input", { + type:"checkbox", + id: autolevelCheckboxId, + checked: action.autoLevel, + changeListener: () => { + action.autoLevel = checkboxInput.checked; + }, +}); + +el.appendChild(checkboxInput); + +*/ \ No newline at end of file diff --git a/src/Bladeburner/ui/ContractList.tsx b/src/Bladeburner/ui/ContractList.tsx new file mode 100644 index 000000000..3e5087eef --- /dev/null +++ b/src/Bladeburner/ui/ContractList.tsx @@ -0,0 +1,25 @@ +import React, { useState, useEffect } from "react"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { ContractElem } from "./ContractElem"; +import { Contract } from "../Contract"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function ContractList(props: IProps): React.ReactElement { + const names = Object.keys(props.bladeburner.contracts); + const contracts = props.bladeburner.contracts; + return (<> + {names.map((name: string) =>
  • + +
  • , + )} + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/ContractPage.tsx b/src/Bladeburner/ui/ContractPage.tsx new file mode 100644 index 000000000..15848d8cf --- /dev/null +++ b/src/Bladeburner/ui/ContractPage.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { ContractList } from "./ContractList"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function ContractPage(props: IProps): React.ReactElement { + return (<> +

    + Complete contracts in order to increase your Bladeburner rank and earn money. + Failing a contract will cause you to lose HP, which can lead to hospitalization. +
    +
    + You can unlock higher-level contracts by successfully completing them. + Higher-level contracts are more difficult, but grant more rank, experience, and money. +

    + + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/GeneralActionElem.tsx b/src/Bladeburner/ui/GeneralActionElem.tsx new file mode 100644 index 000000000..d6a23e8bb --- /dev/null +++ b/src/Bladeburner/ui/GeneralActionElem.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { ActionTypes } from "../data/ActionTypes"; +import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { stealthIcon, killIcon } from "../data/Icons"; +import { BladeburnerConstants } from "../data/Constants"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; + action: any; +} + +export function GeneralActionElem(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + const isActive = props.action.name === props.bladeburner.action.name; + const computedActionTimeCurrent = Math.min(props.bladeburner.actionTimeCurrent+props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeToComplete); + + function onStart() { + props.bladeburner.action.type = ActionTypes[(props.action.name as string)]; + props.bladeburner.action.name = props.action.name; + props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + return (<> +

    + {isActive ? + <>{props.action.name} (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} / {formatNumber(props.bladeburner.actionTimeToComplete, 0)}) : + <>{props.action.name} + } +

    + {isActive ? +

    {createProgressBarText({progress:computedActionTimeCurrent / props.bladeburner.actionTimeToComplete})}

    : + <> + + Start + + } +
    +
    +
    
    +    );
    +}
    \ No newline at end of file
    diff --git a/src/Bladeburner/ui/GeneralActionList.tsx b/src/Bladeburner/ui/GeneralActionList.tsx
    new file mode 100644
    index 000000000..be803bdae
    --- /dev/null
    +++ b/src/Bladeburner/ui/GeneralActionList.tsx
    @@ -0,0 +1,30 @@
    +import React, { useState, useEffect } from "react";
    +import {
    +    formatNumber,
    +    convertTimeMsToTimeElapsedString,
    +} from "../../../utils/StringHelperFunctions";
    +import { GeneralActionElem } from "./GeneralActionElem";
    +import { Action } from "../Action";
    +import { GeneralActions } from "../GeneralActions";
    +import { IBladeburner } from "../IBladeburner";
    +import { IPlayer } from "../../PersonObjects/IPlayer";
    +
    +interface IProps {
    +    bladeburner: IBladeburner;
    +    player: IPlayer;
    +}
    +
    +export function GeneralActionList(props: IProps): React.ReactElement {
    +    const actions: Action[] = [];
    +    for (const name in GeneralActions) {
    +        if (GeneralActions.hasOwnProperty(name)) {
    +            actions.push(GeneralActions[name]);
    +        }
    +    }
    +    return (<>
    +        {actions.map((action: Action) => 
  • + +
  • , + )} + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/GeneralActionPage.tsx b/src/Bladeburner/ui/GeneralActionPage.tsx new file mode 100644 index 000000000..b2647a483 --- /dev/null +++ b/src/Bladeburner/ui/GeneralActionPage.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { GeneralActionList } from "./GeneralActionList"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function GeneralActionPage(props: IProps): React.ReactElement { + return (<> +

    + These are generic actions that will assist you in your Bladeburner + duties. They will not affect your Bladeburner rank in any way. +

    + + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/OperationElem.tsx b/src/Bladeburner/ui/OperationElem.tsx new file mode 100644 index 000000000..482d08703 --- /dev/null +++ b/src/Bladeburner/ui/OperationElem.tsx @@ -0,0 +1,133 @@ +import React, { useState } from "react"; +import { ActionTypes } from "../data/ActionTypes"; +import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { stealthIcon, killIcon } from "../data/Icons"; +import { BladeburnerConstants } from "../data/Constants"; +import { createPopup } from "../../ui/React/createPopup"; +import { TeamSizePopup } from "./TeamSizePopup"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; + action: any; +} + +export function OperationElem(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + const isActive = props.bladeburner.action.type === ActionTypes["Operation"] && props.action.name === props.bladeburner.action.name; + const estimatedSuccessChance = props.action.getSuccessChance(props.bladeburner, {est:true}); + const computedActionTimeCurrent = Math.min(props.bladeburner.actionTimeCurrent+props.bladeburner.actionTimeOverflow,props.bladeburner.actionTimeToComplete); + const maxLevel = (props.action.level >= props.action.maxLevel); + const actionTime = props.action.getActionTime(props.bladeburner); + const autolevelCheckboxId = `bladeburner-${props.action.name}-autolevel-checkbox`; + + function onStart() { + props.bladeburner.action.type = ActionTypes.Operation; + props.bladeburner.action.name = props.action.name; + props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + function onTeam() { + const popupId = "bladeburner-operation-set-team-size-popup"; + createPopup(popupId, TeamSizePopup, { + bladeburner: props.bladeburner, + action: props.action, + popupId: popupId, + }); + } + + function increaseLevel() { + ++props.action.level; + if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + function decreaseLevel() { + --props.action.level; + if (isActive) props.bladeburner.startAction(props.player, props.bladeburner.action); + setRerender(old => !old); + } + + function onAutolevel(event: React.ChangeEvent) { + props.action.autoLevel = event.target.checked; + setRerender(old => !old); + } + + return (<> +

    + {isActive ? + <>{props.action.name} (IN PROGRESS - {formatNumber(computedActionTimeCurrent, 0)} / {formatNumber(props.bladeburner.actionTimeToComplete, 0)}) : + <>{props.action.name} + } +

    + {isActive ? +

    {createProgressBarText({progress:computedActionTimeCurrent / props.bladeburner.actionTimeToComplete})}

    : + <> + + Start + + + Set Team Size (Curr Size: {formatNumber(props.action.teamCount, 0)}) + + } +
    +
    +
    +            
    +                {props.action.getSuccessesNeededForNextLevel(BladeburnerConstants.OperationSuccessesPerLevel)} successes needed for next level
    +            
    +            Level: {props.action.level} / {props.action.maxLevel}
    +        
    + + {isActive && (WARNING: changing the level will restart the Operation)} + ↑ + + + {isActive && (WARNING: changing the level will restart the Operation)} + ↓ + +
    +
    +
    +
    +

    +Estimated success chance: {formatNumber(estimatedSuccessChance*100, 1)}% {props.action.isStealth?stealthIcon:<>}{props.action.isKill?killIcon:<>}
    +Time Required: {convertTimeMsToTimeElapsedString(actionTime*1000)}
    +Operations remaining: {Math.floor(props.action.count)}
    +Successes: {props.action.successes}
    +Failures: {props.action.failures} +
    +
    + + + ); +} diff --git a/src/Bladeburner/ui/OperationList.tsx b/src/Bladeburner/ui/OperationList.tsx new file mode 100644 index 000000000..fa1e7e090 --- /dev/null +++ b/src/Bladeburner/ui/OperationList.tsx @@ -0,0 +1,25 @@ +import React, { useState, useEffect } from "react"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { OperationElem } from "./OperationElem"; +import { Operation } from "../Operation"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function OperationList(props: IProps): React.ReactElement { + const names = Object.keys(props.bladeburner.operations); + const operations = props.bladeburner.operations; + return (<> + {names.map((name: string) =>
  • + +
  • , + )} + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/OperationPage.tsx b/src/Bladeburner/ui/OperationPage.tsx new file mode 100644 index 000000000..f2560f6c0 --- /dev/null +++ b/src/Bladeburner/ui/OperationPage.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { OperationList } from "./OperationList"; +import { IBladeburner } from "../IBladeburner"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + bladeburner: IBladeburner; + player: IPlayer; +} + +export function OperationPage(props: IProps): React.ReactElement { + return (<> +

    + Carry out operations for the Bladeburner division. + Failing an operation will reduce your Bladeburner rank. It will also + cause you to lose HP, which can lead to hospitalization. In general, + operations are harder and more punishing than contracts, + but are also more rewarding. +
    +
    + Operations can affect the chaos level and Synthoid population of your + current city. The exact effects vary between different Operations. +
    +
    + For operations, you can use a team. You must first recruit team members. + Having a larger team will improves your chances of success. +
    +
    + You can unlock higher-level operations by successfully completing them. + Higher-level operations are more difficult, but grant more rank and experience. +

    + + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/Root.tsx b/src/Bladeburner/ui/Root.tsx new file mode 100644 index 000000000..7bb47d55e --- /dev/null +++ b/src/Bladeburner/ui/Root.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Stats } from "./Stats"; +import { Console } from "./Console"; +import { AllPages } from "./AllPages"; + +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { IEngine } from "../../IEngine"; +import { IBladeburner } from "../IBladeburner"; + +interface IProps { + bladeburner: IBladeburner; + engine: IEngine; + player: IPlayer; +} + +export function Root(props: IProps): React.ReactElement { + return (
    +
    +
    + +
    + +
    +
    + +
    +
    ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/SkillElem.tsx b/src/Bladeburner/ui/SkillElem.tsx new file mode 100644 index 000000000..a7cf6341a --- /dev/null +++ b/src/Bladeburner/ui/SkillElem.tsx @@ -0,0 +1,48 @@ +import React, { useState } from "react"; +import { CopyableText } from "../../ui/React/CopyableText"; +import { formatNumber } from "../../../utils/StringHelperFunctions"; +import { IBladeburner } from "../IBladeburner"; + +interface IProps { + skill: any; + bladeburner: IBladeburner; + onUpgrade: () => void; +} + +export function SkillElem(props: IProps): React.ReactElement { + const skillName = props.skill.name; + let currentLevel = 0; + if (props.bladeburner.skills[skillName] && !isNaN(props.bladeburner.skills[skillName])) { + currentLevel = props.bladeburner.skills[skillName]; + } + const pointCost = props.skill.calculateCost(currentLevel); + + const canLevel = props.bladeburner.skillPoints >= pointCost; + const maxLvl = props.skill.maxLvl ? currentLevel >= props.skill.maxLvl : false; + + function onClick() { + if (props.bladeburner.skillPoints < pointCost) return; + props.bladeburner.skillPoints -= pointCost; + props.bladeburner.upgradeSkill(props.skill); + props.onUpgrade(); + } + + return (<> +

    + +

    + + Level + +
    +
    +

    Level: {currentLevel}

    + {maxLvl ? +

    MAX LEVEL

    : +

    Skill Points required: {formatNumber(pointCost, 0)}

    } +

    + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/SkillList.tsx b/src/Bladeburner/ui/SkillList.tsx new file mode 100644 index 000000000..71c67692d --- /dev/null +++ b/src/Bladeburner/ui/SkillList.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import { SkillElem } from "./SkillElem"; +import { Skills } from "../Skills"; +import { IBladeburner } from "../IBladeburner"; + +interface IProps { + bladeburner: IBladeburner; + onUpgrade: () => void; +} + +export function SkillList(props: IProps): React.ReactElement { + return (<> + {Object.keys(Skills).map((skill: string) =>

  • + +
  • , + )} + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/SkillPage.tsx b/src/Bladeburner/ui/SkillPage.tsx new file mode 100644 index 000000000..48c206a00 --- /dev/null +++ b/src/Bladeburner/ui/SkillPage.tsx @@ -0,0 +1,70 @@ +import React, { useState } from "react"; +import { SkillList } from "./SkillList"; +import { BladeburnerConstants } from "../data/Constants"; +import { formatNumber } from "../../../utils/StringHelperFunctions"; +import { IBladeburner } from "../IBladeburner"; + +interface IProps { + bladeburner: IBladeburner; +} + + +export function SkillPage(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + const mults = props.bladeburner.skillMultipliers; + + function valid(mult: any) { + return mult && mult !== 1 + } + + return (<> +

    + Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)} +

    +

    + You will gain one skill point every {BladeburnerConstants.RanksPerSkillPoint} ranks. +
    +
    + Note that when upgrading a skill, the benefit for that skill is additive. + However, the effects of different skills with each other is multiplicative. +
    +

    +
    + {valid(mults["successChanceAll"]) &&

    Total Success Chance: x{formatNumber(mults["successChanceAll"], 3)}

    } + {valid(mults["successChanceStealth"]) &&

    Stealth Success Chance: x{formatNumber(mults["successChanceStealth"], 3)}

    } + {valid(mults["successChanceKill"]) &&

    Retirement Success Chance: x{formatNumber(mults["successChanceKill"], 3)}

    } + {valid(mults["successChanceContract"]) &&

    Contract Success Chance: x{formatNumber(mults["successChanceContract"], 3)}

    } + {valid(mults["successChanceOperation"]) &&

    Operation Success Chance: x{formatNumber(mults["successChanceOperation"], 3)}

    } + {valid(mults["successChanceEstimate"]) &&

    Synthoid Data Estimate: x{formatNumber(mults["successChanceEstimate"], 3)}

    } + {valid(mults["actionTime"]) &&

    Action Time: x{formatNumber(mults["actionTime"], 3)}

    } + {valid(mults["effHack"]) &&

    Hacking Skill: x{formatNumber(mults["effHack"], 3)}

    } + {valid(mults["effStr"]) &&

    Strength: x{formatNumber(mults["effStr"], 3)}

    } + {valid(mults["effDef"]) &&

    Defense: x{formatNumber(mults["effDef"], 3)}

    } + {valid(mults["effDex"]) &&

    Dexterity: x{formatNumber(mults["effDex"], 3)}

    } + {valid(mults["effAgi"]) &&

    Agility: x{formatNumber(mults["effAgi"], 3)}

    } + {valid(mults["effCha"]) &&

    Charisma: x{formatNumber(mults["effCha"], 3)}

    } + {valid(mults["effInt"]) &&

    Intelligence: x{formatNumber(mults["effInt"], 3)}

    } + {valid(mults["stamina"]) &&

    Stamina: x{formatNumber(mults["stamina"], 3)}

    } + {valid(mults["money"]) &&

    Contract Money: x{formatNumber(mults["money"], 3)}

    } + {valid(mults["expGain"]) &&

    Exp Gain: x{formatNumber(mults["expGain"], 3)}

    } +
    + setRerender(old => !old)} /> + ); +} + +/* + + + + +var multKeys = Object.keys(this.skillMultipliers); +for (var i = 0; i < multKeys.length; ++i) { + var mult = this.skillMultipliers[multKeys[i]]; + if (mult && mult !== 1) { + mult = formatNumber(mult, 3); + switch(multKeys[i]) { + + } + } +} +*/ \ No newline at end of file diff --git a/src/Bladeburner/ui/Stats.tsx b/src/Bladeburner/ui/Stats.tsx new file mode 100644 index 000000000..672217a36 --- /dev/null +++ b/src/Bladeburner/ui/Stats.tsx @@ -0,0 +1,139 @@ +import React, { useState, useEffect } from "react"; +import { + formatNumber, + convertTimeMsToTimeElapsedString, +} from "../../../utils/StringHelperFunctions"; +import { BladeburnerConstants } from "../data/Constants"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { IEngine } from "../../IEngine"; +import { Money } from "../../ui/React/Money"; +import { StatsTable } from "../../ui/React/StatsTable"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { createPopup } from "../../ui/React/createPopup"; +import { Factions } from "../../Faction/Factions"; +import { + joinFaction, + displayFactionContent, +} from "../../Faction/FactionHelpers"; +import { IBladeburner } from "../IBladeburner" + +import { TravelPopup } from "./TravelPopup"; + +interface IProps { + bladeburner: IBladeburner; + engine: IEngine; + player: IPlayer; +} + +export function Stats(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + + useEffect(() => { + const id = setInterval(() => setRerender(old => !old), 1000); + return () => clearInterval(id); + }, []); + + function openStaminaHelp(): void { + dialogBoxCreate("Performing actions will use up your stamina.

    " + + "Your max stamina is determined primarily by your agility stat.

    " + + "Your stamina gain rate is determined by both your agility and your " + + "max stamina. Higher max stamina leads to a higher gain rate.

    " + + "Once your " + + "stamina falls below 50% of its max value, it begins to negatively " + + "affect the success rate of your contracts/operations. This penalty " + + "is shown in the overview panel. If the penalty is 15%, then this means " + + "your success rate would be multipled by 85% (100 - 15).

    " + + "Your max stamina and stamina gain rate can also be increased by " + + "training, or through skills and Augmentation upgrades."); + } + + function openPopulationHelp(): void { + dialogBoxCreate("The success rate of your contracts/operations depends on " + + "the population of Synthoids in your current city. " + + "The success rate that is shown to you is only an estimate, " + + "and it is based on your Synthoid population estimate.

    " + + "Therefore, it is important that this Synthoid population estimate " + + "is accurate so that you have a better idea of your " + + "success rate for contracts/operations. Certain " + + "actions will increase the accuracy of your population " + + "estimate.

    " + + "The Synthoid populations of cities can change due to your " + + "actions or random events. If random events occur, they will " + + "be logged in the Bladeburner Console."); + } + + function openTravel() { + const popupId = "bladeburner-travel-popup"; + createPopup(popupId, TravelPopup, { + bladeburner: props.bladeburner, + popupId: popupId, + }); + } + + function openFaction() { + const faction = Factions["Bladeburners"]; + if (faction.isMember) { + props.engine.loadFactionContent(); + displayFactionContent("Bladeburners"); + } else { + if (props.bladeburner.rank >= BladeburnerConstants.RankNeededForFaction) { + joinFaction(faction); + dialogBoxCreate("Congratulations! You were accepted into the Bladeburners faction"); + } else { + dialogBoxCreate("You need a rank of 25 to join the Bladeburners Faction!") + } + } + } + + return (<> +

    + Rank: {formatNumber(props.bladeburner.rank, 2)} + Your rank within the Bladeburner division. +


    +

    Stamina: {formatNumber(props.bladeburner.stamina, 3)} / {formatNumber(props.bladeburner.maxStamina, 3)}

    +
    ?

    +

    Stamina Penalty: {formatNumber((1-props.bladeburner.calculateStaminaPenalty())*100, 1)}%


    +

    Team Size: {formatNumber(props.bladeburner.teamSize, 0)}

    +

    Team Members Lost: {formatNumber(props.bladeburner.teamLost, 0)}


    +

    Num Times Hospitalized: {props.bladeburner.numHosp}

    +

    Money Lost From Hospitalizations: {Money(props.bladeburner.moneyLost)}


    +

    Current City: {props.bladeburner.city}

    +

    + Est. Synthoid Population: {numeralWrapper.formatPopulation(props.bladeburner.getCurrentCity().popEst)} + This is your Bladeburner division's estimate of how many Synthoids exist in your current city. +

    +
    ?

    +

    + Est. Synthoid Communities: {formatNumber(props.bladeburner.getCurrentCity().comms, 0)} + This is your Bladeburner divison's estimate of how many Synthoid communities exist in your current city. +


    +

    + City Chaos: {formatNumber(props.bladeburner.getCurrentCity().chaos)} + The city's chaos level due to tensions and conflicts between humans and Synthoids. Having too high of a chaos level can make contracts and operations harder. +


    +
    +

    + Bonus time: {convertTimeMsToTimeElapsedString(props.bladeburner.storedCycles/BladeburnerConstants.CyclesPerSecond*1000)}
    + + You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by browser). + Bonus time makes the Bladeburner mechanic progress faster, up to 5x the normal speed. + +

    +

    Skill Points: {formatNumber(props.bladeburner.skillPoints, 0)}


    + {StatsTable([ + ["Aug. Success Chance mult: ", formatNumber(props.player.bladeburner_success_chance_mult*100, 1) + "%"], + ["Aug. Max Stamina mult: ", formatNumber(props.player.bladeburner_max_stamina_mult*100, 1) + "%"], + ["Aug. Stamina Gain mult: ", formatNumber(props.player.bladeburner_stamina_gain_mult*100, 1) + "%"], + ["Aug. Field Analysis mult: ", formatNumber(props.player.bladeburner_analysis_mult*100, 1) + "%"], + ])} +
    + Travel + + Apply to the Bladeburner Faction, or go to the faction page if you are already a member + Faction + +
    +
    + ); +} diff --git a/src/Bladeburner/ui/TeamSizePopup.tsx b/src/Bladeburner/ui/TeamSizePopup.tsx new file mode 100644 index 000000000..85973cf9f --- /dev/null +++ b/src/Bladeburner/ui/TeamSizePopup.tsx @@ -0,0 +1,38 @@ +import React, { useState } from "react"; +import { removePopup } from "../../ui/React/createPopup"; +import { BladeburnerConstants } from "../data/Constants"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { Action } from "../Action"; +import { IBladeburner } from "../IBladeburner"; + +interface IProps { + bladeburner: IBladeburner; + action: Action; + popupId: string; +} + +export function TeamSizePopup(props: IProps): React.ReactElement { + const [teamSize, setTeamSize] = useState(); + + function confirmTeamSize(): void { + if(teamSize === undefined) return; + const num = Math.round(teamSize); + if (isNaN(num) || num < 0) { + dialogBoxCreate("Invalid value entered for number of Team Members (must be numeric, positive)") + } else { + props.action.teamCount = num; + } + removePopup(props.popupId); + } + + return (<> +

    + Enter the amount of team members you would like to take on this + Op. If you do not have the specified number of team members, + then as many as possible will be used. Note that team members may + be lost during operations. +

    + setTeamSize(parseFloat(event.target.value))} /> + Confirm + ); +} \ No newline at end of file diff --git a/src/Bladeburner/ui/TravelPopup.tsx b/src/Bladeburner/ui/TravelPopup.tsx new file mode 100644 index 000000000..ef1ec83dc --- /dev/null +++ b/src/Bladeburner/ui/TravelPopup.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { removePopup } from "../../ui/React/createPopup"; +import { BladeburnerConstants } from "../data/Constants"; +import { IBladeburner } from "../IBladeburner"; + +interface IProps { + bladeburner: IBladeburner; + popupId: string; +} + +export function TravelPopup(props: IProps): React.ReactElement { + function travel(city: string) { + props.bladeburner.city = city; + removePopup(props.popupId); + } + + return (<> +

    + Travel to a different city for your Bladeburner + activities. This does not cost any money. The city you are + in for your Bladeburner duties does not affect + your location in the game otherwise. +

    + {BladeburnerConstants.CityNames.map(city => + // Reusing this css class...it adds a border and makes it + // so that background color changes when you hover +
    travel(city)}> + {city} +
    )} + ); +} \ No newline at end of file diff --git a/src/DevMenu.jsx b/src/DevMenu.jsx index 8469995e6..ad14b034d 100644 --- a/src/DevMenu.jsx +++ b/src/DevMenu.jsx @@ -14,7 +14,7 @@ import { AllServers } from "./Server/AllServers"; import { GetServerByHostname } from "./Server/ServerHelpers"; import { hackWorldDaemon } from "./RedPill"; import { StockMarket } from "./StockMarket/StockMarket"; -import { Bladeburner } from "./Bladeburner"; +import { Bladeburner } from "./Bladeburner/Bladeburner"; import { Stock } from "./StockMarket/Stock"; import { Engine } from "./engine"; import { saveObject } from "./SaveObject"; diff --git a/src/Faction/ui/FactionList.tsx b/src/Faction/ui/FactionList.tsx index 3b001a4a3..15a6bda48 100644 --- a/src/Faction/ui/FactionList.tsx +++ b/src/Faction/ui/FactionList.tsx @@ -28,8 +28,7 @@ export function FactionList(props: IProps): React.ReactElement {

    Lists all factions you have joined