From 6669c4da6a91ede1f203c9c6021dbf74e149ad6f Mon Sep 17 00:00:00 2001 From: Snarling <84951833+Snarling@users.noreply.github.com> Date: Thu, 28 Mar 2024 21:52:37 -0400 Subject: [PATCH] BLADEBURNER: Typesafety / refactoring (#1154) --- src/Achievements/Achievements.ts | 11 +- src/Bladeburner/Action.tsx | 306 -- src/Bladeburner/ActionIdentifier.ts | 26 - src/Bladeburner/Actions/Action.ts | 183 ++ src/Bladeburner/Actions/BlackOperation.ts | 51 + src/Bladeburner/Actions/Contract.ts | 33 + src/Bladeburner/Actions/GeneralAction.ts | 35 + src/Bladeburner/Actions/LevelableAction.ts | 111 + src/Bladeburner/Actions/Operation.ts | 84 + src/Bladeburner/Actions/index.ts | 7 + src/Bladeburner/BlackOperation.ts | 32 - src/Bladeburner/BlackOperations.tsx | 571 ---- src/Bladeburner/Bladeburner.ts | 1435 ++++++++++ src/Bladeburner/Bladeburner.tsx | 2454 ----------------- src/Bladeburner/City.ts | 151 +- src/Bladeburner/Contract.ts | 23 - src/Bladeburner/Enums.ts | 62 +- src/Bladeburner/GeneralActions.tsx | 18 - src/Bladeburner/Operation.ts | 56 - src/Bladeburner/Skill.ts | 170 +- src/Bladeburner/Types.ts | 31 + src/Bladeburner/data/ActionTypes.ts | 29 - src/Bladeburner/data/BlackOperations.ts | 737 +++++ src/Bladeburner/data/BlackOperations.tsx | 302 -- src/Bladeburner/data/Contracts.ts | 121 + src/Bladeburner/data/Contracts.tsx | 42 - src/Bladeburner/data/GeneralActions.ts | 58 + src/Bladeburner/data/GeneralActions.tsx | 97 - src/Bladeburner/data/Growths.ts | 24 - src/Bladeburner/data/Operations.ts | 220 ++ src/Bladeburner/data/Operations.tsx | 58 - src/Bladeburner/data/SkillNames.ts | 16 - src/Bladeburner/{ => data}/Skills.ts | 115 +- src/Bladeburner/ui/ActionLevel.tsx | 20 +- src/Bladeburner/ui/AllPages.tsx | 18 +- src/Bladeburner/ui/Autolevel.tsx | 22 +- src/Bladeburner/ui/BlackOpElem.tsx | 60 +- src/Bladeburner/ui/BlackOpList.tsx | 33 +- src/Bladeburner/ui/BlackOpPage.tsx | 11 +- src/Bladeburner/ui/ContractElem.tsx | 61 +- src/Bladeburner/ui/ContractList.tsx | 14 +- src/Bladeburner/ui/GeneralActionElem.tsx | 58 +- src/Bladeburner/ui/GeneralActionList.tsx | 18 +- src/Bladeburner/ui/OperationElem.tsx | 67 +- src/Bladeburner/ui/OperationList.tsx | 14 +- src/Bladeburner/ui/SkillElem.tsx | 31 +- src/Bladeburner/ui/SkillList.tsx | 19 - src/Bladeburner/ui/SkillPage.tsx | 74 +- src/Bladeburner/ui/StartButton.tsx | 48 +- src/Bladeburner/ui/Stats.tsx | 50 +- src/Bladeburner/ui/StealthIcon.tsx | 3 +- src/Bladeburner/ui/SuccessChance.tsx | 32 +- src/Bladeburner/ui/TeamSizeButton.tsx | 18 +- src/Bladeburner/ui/TeamSizeModal.tsx | 4 +- src/Bladeburner/utils/loadActionIdentifier.ts | 25 + src/Company/Companies.ts | 3 +- src/Corporation/Enums.ts | 2 +- src/DevMenu/ui/BladeburnerDev.tsx | 23 +- src/DevMenu/ui/General.tsx | 3 +- src/Faction/Factions.ts | 3 +- src/Go/SaveLoad.ts | 2 +- src/Netscript/NetscriptHelpers.tsx | 21 +- src/NetscriptFunctions/Bladeburner.ts | 304 +- src/NetscriptFunctions/Singularity.ts | 4 +- src/NetscriptFunctions/Sleeve.ts | 26 +- src/PersonObjects/Person.ts | 1 - src/PersonObjects/PersonMethods.ts | 27 - .../Player/PlayerObjectBladeburnerMethods.ts | 1 + src/PersonObjects/Sleeve/Sleeve.ts | 63 +- .../Sleeve/Work/SleeveBladeburnerWork.ts | 55 +- src/PersonObjects/Sleeve/ui/SleeveElem.tsx | 2 +- src/PersonObjects/Sleeve/ui/TaskSelector.tsx | 44 +- src/Prestige.ts | 2 +- src/RemoteFileAPI/MessageDefinitions.ts | 2 +- src/ui/React/CharacterOverview.tsx | 29 +- src/utils/JSONReviver.ts | 13 +- src/utils/TypeAssertion.ts | 4 + src/utils/helpers/clampNumber.ts | 12 +- test/jest/__snapshots__/FullSave.test.ts.snap | 323 +-- 79 files changed, 3876 insertions(+), 5462 deletions(-) delete mode 100644 src/Bladeburner/Action.tsx delete mode 100644 src/Bladeburner/ActionIdentifier.ts create mode 100644 src/Bladeburner/Actions/Action.ts create mode 100644 src/Bladeburner/Actions/BlackOperation.ts create mode 100644 src/Bladeburner/Actions/Contract.ts create mode 100644 src/Bladeburner/Actions/GeneralAction.ts create mode 100644 src/Bladeburner/Actions/LevelableAction.ts create mode 100644 src/Bladeburner/Actions/Operation.ts create mode 100644 src/Bladeburner/Actions/index.ts delete mode 100644 src/Bladeburner/BlackOperation.ts delete mode 100644 src/Bladeburner/BlackOperations.tsx create mode 100644 src/Bladeburner/Bladeburner.ts delete mode 100644 src/Bladeburner/Bladeburner.tsx delete mode 100644 src/Bladeburner/Contract.ts delete mode 100644 src/Bladeburner/GeneralActions.tsx delete mode 100644 src/Bladeburner/Operation.ts create mode 100644 src/Bladeburner/Types.ts delete mode 100644 src/Bladeburner/data/ActionTypes.ts create mode 100644 src/Bladeburner/data/BlackOperations.ts delete mode 100644 src/Bladeburner/data/BlackOperations.tsx create mode 100644 src/Bladeburner/data/Contracts.ts delete mode 100644 src/Bladeburner/data/Contracts.tsx create mode 100644 src/Bladeburner/data/GeneralActions.ts delete mode 100644 src/Bladeburner/data/GeneralActions.tsx delete mode 100644 src/Bladeburner/data/Growths.ts create mode 100644 src/Bladeburner/data/Operations.ts delete mode 100644 src/Bladeburner/data/Operations.tsx delete mode 100644 src/Bladeburner/data/SkillNames.ts rename src/Bladeburner/{ => data}/Skills.ts (50%) delete mode 100644 src/Bladeburner/ui/SkillList.tsx create mode 100644 src/Bladeburner/utils/loadActionIdentifier.ts create mode 100644 src/utils/TypeAssertion.ts diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts index 374387120..7b37cf9de 100644 --- a/src/Achievements/Achievements.ts +++ b/src/Achievements/Achievements.ts @@ -1,14 +1,13 @@ import { AugmentationName, - BlackOperationName, + BladeSkillName, CityName, CompletedProgramName, CorpUnlockName, FactionName, IndustryType, } from "@enums"; -import { SkillNames } from "../Bladeburner/data/SkillNames"; -import { Skills } from "../Bladeburner/Skills"; +import { Skills } from "../Bladeburner/data/Skills"; import { CONSTANTS } from "../Constants"; import { Exploit } from "../Exploits/Exploit"; import { Factions } from "../Faction/Factions"; @@ -31,6 +30,7 @@ import { workerScripts } from "../Netscript/WorkerScripts"; import { getRecordValues } from "../Types/Record"; import { ServerConstants } from "../Server/data/Constants"; +import { blackOpsArray } from "../Bladeburner/data/BlackOperations"; // Unable to correctly cast the JSON data into AchievementDataJson type otherwise... const achievementData = ((data)).achievements; @@ -65,7 +65,7 @@ function bitNodeFinishedState(): boolean { const wd = GetServer(SpecialServers.WorldDaemon); if (!(wd instanceof Server)) return false; if (wd.backdoorInstalled) return true; - return Player.bladeburner !== null && BlackOperationName.OperationDaedalus in Player.bladeburner.blackops; + return Player.bladeburner !== null && Player.bladeburner.numBlackOpsComplete >= blackOpsArray.length; } function hasAccessToSF(bn: number): boolean { @@ -432,8 +432,7 @@ export const achievements: Record = { Icon: "BLADEOVERCLOCK", Visible: () => hasAccessToSF(6), Condition: () => - Player.bladeburner !== null && - Player.bladeburner.skills[SkillNames.Overclock] === Skills[SkillNames.Overclock].maxLvl, + Player.bladeburner?.getSkillLevel(BladeSkillName.overclock) === Skills[BladeSkillName.overclock].maxLvl, }, BLADEBURNER_UNSPENT_100000: { ...achievementData.BLADEBURNER_UNSPENT_100000, diff --git a/src/Bladeburner/Action.tsx b/src/Bladeburner/Action.tsx deleted file mode 100644 index 143c000dc..000000000 --- a/src/Bladeburner/Action.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { getRandomInt } from "../utils/helpers/getRandomInt"; -import { addOffset } from "../utils/helpers/addOffset"; -import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; -import { BladeburnerConstants } from "./data/Constants"; -import { Bladeburner } from "./Bladeburner"; -import { Person } from "../PersonObjects/Person"; -import { calculateIntelligenceBonus } from "../PersonObjects/formulas/intelligence"; - -interface ISuccessChanceParams { - est: boolean; -} - -class StatsMultiplier { - [key: string]: number; - - hack = 0; - str = 0; - def = 0; - dex = 0; - agi = 0; - cha = 0; - int = 0; -} - -export interface IActionParams { - name?: string; - level?: number; - maxLevel?: number; - autoLevel?: boolean; - baseDifficulty?: number; - difficultyFac?: number; - rewardFac?: number; - successes?: number; - failures?: number; - rankGain?: number; - rankLoss?: number; - hpLoss?: number; - hpLost?: number; - isStealth?: boolean; - isKill?: boolean; - count?: number; - weights?: StatsMultiplier; - decays?: StatsMultiplier; - teamCount?: number; -} - -export class Action { - name = ""; - - // Difficulty scales with level. See getDifficulty() method - level = 1; - maxLevel = 1; - autoLevel = true; - baseDifficulty = 100; - difficultyFac = 1.01; - - // Rank increase/decrease is affected by this exponent - rewardFac = 1.02; - - successes = 0; - failures = 0; - - // All of these scale with level/difficulty - rankGain = 0; - rankLoss = 0; - hpLoss = 0; - hpLost = 0; - - // Action Category. Current categories are stealth and kill - isStealth = false; - isKill = false; - - /** - * Number of this contract remaining, and its growth rate - * Growth rate is an integer and the count will increase by that integer every "cycle" - */ - count: number = getRandomInt(1e3, 25e3); - - // Weighting of each stat in determining action success rate - weights: StatsMultiplier = { - hack: 1 / 7, - str: 1 / 7, - def: 1 / 7, - dex: 1 / 7, - agi: 1 / 7, - cha: 1 / 7, - int: 1 / 7, - }; - // Diminishing returns of stats (stat ^ decay where 0 <= decay <= 1) - decays: StatsMultiplier = { - hack: 0.9, - str: 0.9, - def: 0.9, - dex: 0.9, - agi: 0.9, - cha: 0.9, - int: 0.9, - }; - teamCount = 0; - - // Base Class for Contracts, Operations, and BlackOps - constructor(params: IActionParams | null = null) { - // | null = null - if (params && params.name) this.name = params.name; - - if (params && params.baseDifficulty) this.baseDifficulty = addOffset(params.baseDifficulty, 10); - if (params && params.difficultyFac) this.difficultyFac = params.difficultyFac; - - if (params && params.rewardFac) this.rewardFac = params.rewardFac; - if (params && params.rankGain) this.rankGain = params.rankGain; - if (params && params.rankLoss) this.rankLoss = params.rankLoss; - if (params && params.hpLoss) this.hpLoss = params.hpLoss; - - if (params && params.isStealth) this.isStealth = params.isStealth; - if (params && params.isKill) this.isKill = params.isKill; - - if (params && params.count) this.count = params.count; - - if (params && params.weights) this.weights = params.weights; - if (params && params.decays) this.decays = params.decays; - - // Check to make sure weights are summed properly - let sum = 0; - for (const weight of Object.keys(this.weights)) { - if (Object.hasOwn(this.weights, weight)) { - sum += this.weights[weight]; - } - } - if (sum - 1 >= 10 * Number.EPSILON) { - throw new Error( - "Invalid weights when constructing Action " + - this.name + - ". The weights should sum up to 1. They sum up to :" + - 1, - ); - } - - for (const decay of Object.keys(this.decays)) { - if (Object.hasOwn(this.decays, decay)) { - if (this.decays[decay] > 1) { - throw new Error(`Invalid decays when constructing Action ${this.name}. Decay value cannot be greater than 1`); - } - } - } - } - - getDifficulty(): number { - const difficulty = this.baseDifficulty * Math.pow(this.difficultyFac, this.level - 1); - if (isNaN(difficulty)) { - throw new Error("Calculated NaN in Action.getDifficulty()"); - } - return difficulty; - } - - /** - * Tests for success. Should be called when an action has completed - * @param inst {Bladeburner} - Bladeburner instance - */ - attempt(inst: Bladeburner, person: Person): boolean { - return Math.random() < this.getSuccessChance(inst, person); - } - - // To be implemented by subtypes - getActionTimePenalty(): number { - return 1; - } - - getActionTime(inst: Bladeburner, person: Person): number { - const difficulty = this.getDifficulty(); - let baseTime = difficulty / BladeburnerConstants.DifficultyToTimeFactor; - const skillFac = inst.skillMultipliers.actionTime; // Always < 1 - - const effAgility = person.skills.agility * inst.skillMultipliers.effAgi; - const effDexterity = person.skills.dexterity * inst.skillMultipliers.effDex; - const statFac = - 0.5 * - (Math.pow(effAgility, BladeburnerConstants.EffAgiExponentialFactor) + - Math.pow(effDexterity, BladeburnerConstants.EffDexExponentialFactor) + - effAgility / BladeburnerConstants.EffAgiLinearFactor + - effDexterity / BladeburnerConstants.EffDexLinearFactor); // Always > 1 - - baseTime = Math.max(1, (baseTime * skillFac) / statFac); - - return Math.ceil(baseTime * this.getActionTimePenalty()); - } - - // Subtypes of Action implement these differently - getTeamSuccessBonus(__inst: Bladeburner): number { - return 1; - } - - getActionTypeSkillSuccessBonus(__inst: Bladeburner): number { - return 1; - } - - getChaosCompetencePenalty(inst: Bladeburner, params: ISuccessChanceParams): number { - const city = inst.getCurrentCity(); - if (params.est) { - return Math.pow(city.popEst / BladeburnerConstants.PopulationThreshold, BladeburnerConstants.PopulationExponent); - } else { - return Math.pow(city.pop / BladeburnerConstants.PopulationThreshold, BladeburnerConstants.PopulationExponent); - } - } - - getChaosDifficultyBonus(inst: Bladeburner /*, params: ISuccessChanceParams*/): number { - const city = inst.getCurrentCity(); - if (city.chaos > BladeburnerConstants.ChaosThreshold) { - const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold); - const mult = Math.pow(diff, 0.5); - return mult; - } - - return 1; - } - - getEstSuccessChance(inst: Bladeburner, person: Person): [number, number] { - function clamp(x: number): number { - return Math.max(0, Math.min(x, 1)); - } - const est = this.getSuccessChance(inst, person, { est: true }); - const real = this.getSuccessChance(inst, person); - const diff = Math.abs(real - est); - let low = real - diff; - let high = real + diff; - const city = inst.getCurrentCity(); - let r = city.pop / city.popEst; - if (Number.isNaN(r)) r = 0; - if (r < 1) low *= r; - else high *= r; - return [clamp(low), clamp(high)]; - } - - /** - * @inst - Bladeburner Object - * @params - options: - * est (bool): Get success chance estimate instead of real success chance - */ - getSuccessChance(inst: Bladeburner, person: Person, params: ISuccessChanceParams = { est: false }): number { - if (inst == null) { - throw new Error("Invalid Bladeburner instance passed into Action.getSuccessChance"); - } - let difficulty = this.getDifficulty(); - let competence = 0; - for (const stat of Object.keys(this.weights)) { - if (Object.hasOwn(this.weights, stat)) { - const playerStatLvl = person.queryStatFromString(stat); - const key = "eff" + stat.charAt(0).toUpperCase() + stat.slice(1); - let effMultiplier = inst.skillMultipliers[key]; - if (effMultiplier == null) { - console.error(`Failed to find Bladeburner Skill multiplier for: ${stat}`); - effMultiplier = 1; - } - competence += this.weights[stat] * Math.pow(effMultiplier * playerStatLvl, this.decays[stat]); - } - } - competence *= calculateIntelligenceBonus(person.skills.intelligence, 0.75); - competence *= inst.calculateStaminaPenalty(); - - competence *= this.getTeamSuccessBonus(inst); - - competence *= this.getChaosCompetencePenalty(inst, params); - difficulty *= this.getChaosDifficultyBonus(inst); - - if (this.name == "Raid" && inst.getCurrentCity().comms <= 0) { - return 0; - } - - // Factor skill multipliers into success chance - competence *= inst.skillMultipliers.successChanceAll; - competence *= this.getActionTypeSkillSuccessBonus(inst); - if (this.isStealth) { - competence *= inst.skillMultipliers.successChanceStealth; - } - if (this.isKill) { - competence *= inst.skillMultipliers.successChanceKill; - } - - // Augmentation multiplier - competence *= person.mults.bladeburner_success_chance; - - if (isNaN(competence)) { - throw new Error("Competence calculated as NaN in Action.getSuccessChance()"); - } - return Math.min(1, competence / difficulty); - } - - getSuccessesNeededForNextLevel(baseSuccessesPerLevel: number): number { - return Math.ceil(0.5 * this.maxLevel * (2 * baseSuccessesPerLevel + (this.maxLevel - 1))); - } - - setMaxLevel(baseSuccessesPerLevel: number): void { - if (this.successes >= this.getSuccessesNeededForNextLevel(baseSuccessesPerLevel)) { - ++this.maxLevel; - } - } - - toJSON(): IReviverValue { - return Generic_toJSON("Action", this); - } - - static fromJSON(value: IReviverValue): Action { - return Generic_fromJSON(Action, value.data); - } -} - -constructorsForReviver.Action = Action; diff --git a/src/Bladeburner/ActionIdentifier.ts b/src/Bladeburner/ActionIdentifier.ts deleted file mode 100644 index 57b7995b8..000000000 --- a/src/Bladeburner/ActionIdentifier.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; - -interface IParams { - name?: string; - type?: number; -} - -export class ActionIdentifier { - name = ""; - type = -1; - - constructor(params: IParams = {}) { - if (params.name) this.name = params.name; - if (params.type) this.type = params.type; - } - - toJSON(): IReviverValue { - return Generic_toJSON("ActionIdentifier", this); - } - - static fromJSON(value: IReviverValue): ActionIdentifier { - return Generic_fromJSON(ActionIdentifier, value.data); - } -} - -constructorsForReviver.ActionIdentifier = ActionIdentifier; diff --git a/src/Bladeburner/Actions/Action.ts b/src/Bladeburner/Actions/Action.ts new file mode 100644 index 000000000..592a95a1d --- /dev/null +++ b/src/Bladeburner/Actions/Action.ts @@ -0,0 +1,183 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { Person } from "../../PersonObjects/Person"; +import type { Availability, SuccessChanceParams } from "../Types"; +import type { Skills as PersonSkills } from "../../PersonObjects/Skills"; + +import { addOffset } from "../../utils/helpers/addOffset"; +import { BladeburnerConstants } from "../data/Constants"; +import { calculateIntelligenceBonus } from "../../PersonObjects/formulas/intelligence"; +import { BladeMultName } from "../Enums"; +import { getRecordKeys } from "../../Types/Record"; + +export interface ActionParams { + desc: string; + baseDifficulty?: number; + rewardFac?: number; + rankGain?: number; + rankLoss?: number; + hpLoss?: number; + isStealth?: boolean; + isKill?: boolean; + weights?: PersonSkills; + decays?: PersonSkills; +} + +export abstract class ActionClass { + desc = ""; + // For LevelableActions, the base difficulty can be increased based on action level + baseDifficulty = 100; + + // All of these scale with level/difficulty + rankGain = 0; + rankLoss = 0; + hpLoss = 0; + + // Action Category. Current categories are stealth and kill + isStealth = false; + isKill = false; + + // Weighting of each stat in determining action success rate + weights: PersonSkills = { + hacking: 1 / 7, + strength: 1 / 7, + defense: 1 / 7, + dexterity: 1 / 7, + agility: 1 / 7, + charisma: 1 / 7, + intelligence: 1 / 7, + }; + // Diminishing returns of stats (stat ^ decay where 0 <= decay <= 1) + decays: PersonSkills = { + hacking: 0.9, + strength: 0.9, + defense: 0.9, + dexterity: 0.9, + agility: 0.9, + charisma: 0.9, + intelligence: 0.9, + }; + + constructor(params: ActionParams | null = null) { + if (!params) return; + this.desc = params.desc; + if (params.baseDifficulty) this.baseDifficulty = addOffset(params.baseDifficulty, 10); + + if (params.rankGain) this.rankGain = params.rankGain; + if (params.rankLoss) this.rankLoss = params.rankLoss; + if (params.hpLoss) this.hpLoss = params.hpLoss; + + if (params.isStealth) this.isStealth = params.isStealth; + if (params.isKill) this.isKill = params.isKill; + + if (params.weights) this.weights = params.weights; + if (params.decays) this.decays = params.decays; + } + + /** Tests for success. Should be called when an action has completed */ + attempt(bladeburner: Bladeburner, person: Person): boolean { + return Math.random() < this.getSuccessChance(bladeburner, person); + } + + // All the functions below are overwritten by certain subtypes of action, e.g. BlackOps ignore city stats + getPopulationSuccessFactor(bladeburner: Bladeburner, { est }: SuccessChanceParams): number { + const city = bladeburner.getCurrentCity(); + const pop = est ? city.popEst : city.pop; + return Math.pow(pop / BladeburnerConstants.PopulationThreshold, BladeburnerConstants.PopulationExponent); + } + + getChaosSuccessFactor(bladeburner: Bladeburner): number { + const city = bladeburner.getCurrentCity(); + if (city.chaos > BladeburnerConstants.ChaosThreshold) { + const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold); + const mult = Math.pow(diff, 0.5); + return mult; + } + + return 1; + } + + getActionTime(bladeburner: Bladeburner, person: Person): number { + const difficulty = this.getDifficulty(); + let baseTime = difficulty / BladeburnerConstants.DifficultyToTimeFactor; + const skillFac = bladeburner.getSkillMult(BladeMultName.actionTime); // Always < 1 + + const effAgility = bladeburner.getEffectiveSkillLevel(person, "agility"); + const effDexterity = bladeburner.getEffectiveSkillLevel(person, "dexterity"); + const statFac = + 0.5 * + (Math.pow(effAgility, BladeburnerConstants.EffAgiExponentialFactor) + + Math.pow(effDexterity, BladeburnerConstants.EffDexExponentialFactor) + + effAgility / BladeburnerConstants.EffAgiLinearFactor + + effDexterity / BladeburnerConstants.EffDexLinearFactor); // Always > 1 + + baseTime = Math.max(1, (baseTime * skillFac) / statFac); + + return Math.ceil(baseTime * this.getActionTimePenalty()); + } + + getTeamSuccessBonus(__bladeburner: Bladeburner): number { + return 1; + } + + getActionTypeSkillSuccessBonus(__bladeburner: Bladeburner): number { + return 1; + } + + getAvailability(__bladeburner: Bladeburner): Availability { + return { available: true }; + } + + getActionTimePenalty(): number { + return 1; + } + + getDifficulty(): number { + return this.baseDifficulty; + } + + getSuccessRange(bladeburner: Bladeburner, person: Person): [minChance: number, maxChance: number] { + function clamp(x: number): number { + return Math.max(0, Math.min(x, 1)); + } + const est = this.getSuccessChance(bladeburner, person, { est: true }); + const real = this.getSuccessChance(bladeburner, person); + const diff = Math.abs(real - est); + let low = real - diff; + let high = real + diff; + const city = bladeburner.getCurrentCity(); + let r = city.pop / city.popEst; + if (Number.isNaN(r)) r = 0; + if (r < 1) low *= r; + else high *= r; + return [clamp(low), clamp(high)]; + } + + getSuccessChance(inst: Bladeburner, person: Person, { est }: SuccessChanceParams = { est: false }): number { + let difficulty = this.getDifficulty(); + let competence = 0; + for (const stat of getRecordKeys(person.skills)) { + competence += this.weights[stat] * Math.pow(inst.getEffectiveSkillLevel(person, stat), this.decays[stat]); + } + competence *= calculateIntelligenceBonus(person.skills.intelligence, 0.75); + competence *= inst.calculateStaminaPenalty(); + + competence *= this.getTeamSuccessBonus(inst); + + competence *= this.getPopulationSuccessFactor(inst, { est }); + difficulty *= this.getChaosSuccessFactor(inst); + + // Factor skill multipliers into success chance + competence *= inst.getSkillMult(BladeMultName.successChanceAll); + competence *= this.getActionTypeSkillSuccessBonus(inst); + if (this.isStealth) competence *= inst.getSkillMult(BladeMultName.successChanceStealth); + if (this.isKill) competence *= inst.getSkillMult(BladeMultName.successChanceKill); + + // Augmentation multiplier + competence *= person.mults.bladeburner_success_chance; + + if (isNaN(competence)) { + throw new Error("Competence calculated as NaN in Action.getSuccessChance()"); + } + return Math.min(1, competence / difficulty); + } +} diff --git a/src/Bladeburner/Actions/BlackOperation.ts b/src/Bladeburner/Actions/BlackOperation.ts new file mode 100644 index 000000000..7614d8a79 --- /dev/null +++ b/src/Bladeburner/Actions/BlackOperation.ts @@ -0,0 +1,51 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { Availability, ActionIdentifier } from "../Types"; + +import { BladeActionType, BladeBlackOpName } from "@enums"; +import { ActionClass, ActionParams } from "./Action"; +import { operationSkillSuccessBonus, operationTeamSuccessBonus } from "./Operation"; + +interface BlackOpParams { + name: BladeBlackOpName; + reqdRank: number; + n: number; +} + +export class BlackOperation extends ActionClass { + type: BladeActionType.blackOp = BladeActionType.blackOp; + name: BladeBlackOpName; + n: number; + reqdRank: number; + teamCount = 0; + get id(): ActionIdentifier { + return { type: this.type, name: this.name }; + } + + constructor(params: ActionParams & BlackOpParams) { + super(params); + this.name = params.name; + this.reqdRank = params.reqdRank; + this.n = params.n; + } + + getAvailability(bladeburner: Bladeburner): Availability { + if (bladeburner.numBlackOpsComplete < this.n) return { error: "Have not completed the previous Black Operation" }; + if (bladeburner.numBlackOpsComplete > this.n) return { error: "Already completed" }; + if (bladeburner.rank < this.reqdRank) return { error: "Insufficient rank" }; + return { available: true }; + } + // To be implemented by subtypes + getActionTimePenalty(): number { + return 1.5; + } + + getPopulationSuccessFactor(/*inst: Bladeburner, params: ISuccessChanceParams*/): number { + return 1; + } + + getChaosSuccessFactor(/*inst: Bladeburner, params: ISuccessChanceParams*/): number { + return 1; + } + getTeamSuccessBonus = operationTeamSuccessBonus; + getActionTypeSkillSuccessBonus = operationSkillSuccessBonus; +} diff --git a/src/Bladeburner/Actions/Contract.ts b/src/Bladeburner/Actions/Contract.ts new file mode 100644 index 000000000..bae9e9638 --- /dev/null +++ b/src/Bladeburner/Actions/Contract.ts @@ -0,0 +1,33 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { ActionIdentifier } from "../Types"; + +import { Generic_fromJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver"; +import { BladeActionType, BladeContractName, BladeMultName } from "../Enums"; +import { LevelableActionClass, LevelableActionParams } from "./LevelableAction"; + +export class Contract extends LevelableActionClass { + type: BladeActionType.contract = BladeActionType.contract; + name: BladeContractName = BladeContractName.tracking; + get id(): ActionIdentifier { + return { type: this.type, name: this.name }; + } + + constructor(params: (LevelableActionParams & { name: BladeContractName }) | null = null) { + super(params); + if (params) this.name = params.name; + } + + getActionTypeSkillSuccessBonus(inst: Bladeburner): number { + return inst.getSkillMult(BladeMultName.successChanceContract); + } + + toJSON(): IReviverValue { + return this.save("Contract"); + } + + static fromJSON(value: IReviverValue): Contract { + return Generic_fromJSON(Contract, value.data); + } +} + +constructorsForReviver.Contract = Contract; diff --git a/src/Bladeburner/Actions/GeneralAction.ts b/src/Bladeburner/Actions/GeneralAction.ts new file mode 100644 index 000000000..169b9ebda --- /dev/null +++ b/src/Bladeburner/Actions/GeneralAction.ts @@ -0,0 +1,35 @@ +import type { Person } from "../../PersonObjects/Person"; +import type { Bladeburner } from "../Bladeburner"; +import type { ActionIdentifier } from "../Types"; + +import { BladeActionType, BladeGeneralActionName } from "@enums"; +import { ActionClass, ActionParams } from "./Action"; + +type GeneralActionParams = ActionParams & { + name: BladeGeneralActionName; + getActionTime: (bladeburner: Bladeburner, person: Person) => number; + getSuccessChance?: (bladeburner: Bladeburner, person: Person) => number; +}; + +export class GeneralAction extends ActionClass { + type: BladeActionType.general = BladeActionType.general; + name: BladeGeneralActionName; + get id(): ActionIdentifier { + return { type: this.type, name: this.name }; + } + + constructor(params: GeneralActionParams) { + super(params); + this.name = params.name; + this.getActionTime = params.getActionTime; + if (params.getSuccessChance) this.getSuccessChance = params.getSuccessChance; + } + + getSuccessChance(__bladeburner: Bladeburner, __person: Person): number { + return 1; + } + getSuccessRange(bladeburner: Bladeburner, person: Person): [minChance: number, maxChance: number] { + const chance = this.getSuccessChance(bladeburner, person); + return [chance, chance]; + } +} diff --git a/src/Bladeburner/Actions/LevelableAction.ts b/src/Bladeburner/Actions/LevelableAction.ts new file mode 100644 index 000000000..d84cfe618 --- /dev/null +++ b/src/Bladeburner/Actions/LevelableAction.ts @@ -0,0 +1,111 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { IReviverValue } from "../../utils/JSONReviver"; +import type { Availability } from "../Types"; + +import { ActionClass, ActionParams } from "./Action"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { clampInteger } from "../../utils/helpers/clampNumber"; + +export type LevelableActionParams = ActionParams & { + growthFunction: () => number; + difficultyFac?: number; + rewardFac?: number; + minCount?: number; + maxCount?: number; +}; + +export abstract class LevelableActionClass extends ActionClass { + // Static info, not included in save + difficultyFac = 1.01; + rewardFac = 1.02; + growthFunction = () => 0; + minCount = 1; + maxCount = 150; + + // Dynamic properties included in save + count = 0; + level = 1; + maxLevel = 1; + autoLevel = true; + successes = 0; + failures = 0; + + constructor(params: LevelableActionParams | null = null) { + super(params); + if (!params) return; + if (params.minCount) this.minCount = params.minCount; + if (params.maxCount) this.maxCount = params.maxCount; + if (params.difficultyFac) this.difficultyFac = params.difficultyFac; + if (params.rewardFac) this.rewardFac = params.rewardFac; + this.count = getRandomInt(this.minCount, this.maxCount); + this.growthFunction = params.growthFunction; + } + + getAvailability(__bladeburner: Bladeburner): Availability { + if (this.count < 1) return { error: "Insufficient action count" }; + return { available: true }; + } + + setMaxLevel(baseSuccessesPerLevel: number): void { + if (this.successes >= this.getSuccessesNeededForNextLevel(baseSuccessesPerLevel)) { + ++this.maxLevel; + } + } + getSuccessesNeededForNextLevel(baseSuccessesPerLevel: number): number { + return Math.ceil(0.5 * this.maxLevel * (2 * baseSuccessesPerLevel + (this.maxLevel - 1))); + } + + getDifficulty() { + const difficulty = this.baseDifficulty * Math.pow(this.difficultyFac, this.level - 1); + if (isNaN(difficulty)) { + throw new Error("Calculated NaN in Action.getDifficulty()"); + } + return difficulty; + } + + /** Reset a levelable action's tracked stats */ + reset() { + this.count = getRandomInt(this.minCount, this.maxCount); + this.level = 1; + this.maxLevel = 1; + this.autoLevel = true; + this.successes = 0; + this.failures = 0; + } + + /** These are not loaded the same way as most game objects, to allow better typechecking on load + partially static loading */ + loadData(loadedObject: LevelableActionClass) { + this.maxLevel = clampInteger(loadedObject.maxLevel, 1); + this.level = clampInteger(loadedObject.level, 1, this.maxLevel); + this.count = clampInteger(loadedObject.count, 0); + this.autoLevel = !!loadedObject.autoLevel; + this.successes = clampInteger(loadedObject.successes, 0); + this.failures = clampInteger(loadedObject.failures, 0); + } + /** Create a basic object just containing the relevant data for a levelable action */ + save( + this: T, + ctorName: string, + ...extraParams: (keyof T)[] + ): IReviverValue { + const data = { + ...Object.fromEntries(extraParams.map((param) => [param, this[param]])), + count: this.count, + level: this.level, + maxLevel: this.maxLevel, + autoLevel: this.autoLevel, + successes: this.successes, + failures: this.failures, + }; + return { ctor: ctorName, data }; + } +} + +export interface LevelableActionSaveData { + count: number; + level: number; + maxLevel: number; + autoLevel: boolean; + successes: number; + failures: number; +} diff --git a/src/Bladeburner/Actions/Operation.ts b/src/Bladeburner/Actions/Operation.ts new file mode 100644 index 000000000..285127fd5 --- /dev/null +++ b/src/Bladeburner/Actions/Operation.ts @@ -0,0 +1,84 @@ +import type { Person } from "../../PersonObjects/Person"; +import type { BlackOperation } from "./BlackOperation"; +import type { Bladeburner } from "../Bladeburner"; +import type { Availability, ActionIdentifier, SuccessChanceParams } from "../Types"; + +import { BladeActionType, BladeMultName, BladeOperationName } from "@enums"; +import { BladeburnerConstants } from "../data/Constants"; +import { ActionClass } from "./Action"; +import { Generic_fromJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver"; +import { LevelableActionClass, LevelableActionParams } from "./LevelableAction"; +import { clampInteger } from "../../utils/helpers/clampNumber"; + +export interface OperationParams extends LevelableActionParams { + name: BladeOperationName; + getAvailability?: (bladeburner: Bladeburner) => Availability; +} + +export class Operation extends LevelableActionClass { + type: BladeActionType.operation = BladeActionType.operation; + name = BladeOperationName.investigation; + teamCount = 0; + get id(): ActionIdentifier { + return { type: this.type, name: this.name }; + } + + constructor(params: OperationParams | null = null) { + super(params); + if (!params) return; + this.name = params.name; + if (params.getAvailability) this.getAvailability = params.getAvailability; + } + + // These functions are shared between operations and blackops, so they are defined outside of Operation + getTeamSuccessBonus = operationTeamSuccessBonus; + getActionTypeSkillSuccessBonus = operationSkillSuccessBonus; + + getChaosSuccessFactor(inst: Bladeburner /*, params: ISuccessChanceParams*/): number { + const city = inst.getCurrentCity(); + if (city.chaos > BladeburnerConstants.ChaosThreshold) { + const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold); + const mult = Math.pow(diff, 0.5); + return mult; + } + + return 1; + } + getSuccessChance(inst: Bladeburner, person: Person, params: SuccessChanceParams) { + if (this.name == BladeOperationName.raid && inst.getCurrentCity().comms <= 0) return 0; + return ActionClass.prototype.getSuccessChance.call(this, inst, person, params); + } + + reset() { + LevelableActionClass.prototype.reset.call(this); + this.teamCount = 0; + } + + toJSON(): IReviverValue { + return this.save("Operation", "teamCount"); + } + loadData(loadedObject: Operation): void { + this.teamCount = clampInteger(loadedObject.teamCount, 0); + LevelableActionClass.prototype.loadData.call(this, loadedObject); + } + + static fromJSON(value: IReviverValue): Operation { + return Generic_fromJSON(Operation, value.data); + } +} + +constructorsForReviver.Operation = Operation; + +// shared member functions for Operation and BlackOperation +export const operationSkillSuccessBonus = (inst: Bladeburner) => { + return inst.getSkillMult(BladeMultName.successChanceOperation); +}; +export function operationTeamSuccessBonus(this: Operation | BlackOperation, inst: Bladeburner) { + if (this.teamCount && this.teamCount > 0) { + this.teamCount = Math.min(this.teamCount, inst.teamSize); + const teamMultiplier = Math.pow(this.teamCount, 0.05); + return teamMultiplier; + } + + return 1; +} diff --git a/src/Bladeburner/Actions/index.ts b/src/Bladeburner/Actions/index.ts new file mode 100644 index 000000000..f4f7dfedf --- /dev/null +++ b/src/Bladeburner/Actions/index.ts @@ -0,0 +1,7 @@ +// Barrel file for easier importing +export { ActionClass } from "./Action"; +export { BlackOperation } from "./BlackOperation"; +export { Contract } from "./Contract"; +export { GeneralAction } from "./GeneralAction"; +export { Operation } from "./Operation"; +export { LevelableActionClass } from "./LevelableAction"; diff --git a/src/Bladeburner/BlackOperation.ts b/src/Bladeburner/BlackOperation.ts deleted file mode 100644 index bdf79f139..000000000 --- a/src/Bladeburner/BlackOperation.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Operation, IOperationParams } from "./Operation"; -import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; - -export class BlackOperation extends Operation { - constructor(params: IOperationParams | null = null) { - super(params); - this.count = 1; - } - - // To be implemented by subtypes - getActionTimePenalty(): number { - return 1.5; - } - - getChaosCompetencePenalty(/*inst: Bladeburner, params: ISuccessChanceParams*/): number { - return 1; - } - - getChaosDifficultyBonus(/*inst: Bladeburner, params: ISuccessChanceParams*/): number { - return 1; - } - - toJSON(): IReviverValue { - return Generic_toJSON("BlackOperation", this); - } - - static fromJSON(value: IReviverValue): Operation { - return Generic_fromJSON(BlackOperation, value.data); - } -} - -constructorsForReviver.BlackOperation = BlackOperation; diff --git a/src/Bladeburner/BlackOperations.tsx b/src/Bladeburner/BlackOperations.tsx deleted file mode 100644 index 8b4830a78..000000000 --- a/src/Bladeburner/BlackOperations.tsx +++ /dev/null @@ -1,571 +0,0 @@ -import { BlackOperation } from "./BlackOperation"; -import { BlackOperationName } from "@enums"; - -export const BlackOperations: Record = {}; - -(function () { - BlackOperations[BlackOperationName.OperationTyphoon] = new BlackOperation({ - name: BlackOperationName.OperationTyphoon, - baseDifficulty: 2000, - reqdRank: 2.5e3, - rankGain: 50, - rankLoss: 10, - hpLoss: 100, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationZero] = new BlackOperation({ - name: BlackOperationName.OperationZero, - baseDifficulty: 2500, - reqdRank: 5e3, - rankGain: 60, - rankLoss: 15, - hpLoss: 50, - weights: { - hack: 0.2, - str: 0.15, - def: 0.15, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isStealth: true, - }); - BlackOperations[BlackOperationName.OperationX] = new BlackOperation({ - name: BlackOperationName.OperationX, - baseDifficulty: 3000, - reqdRank: 7.5e3, - rankGain: 75, - rankLoss: 15, - hpLoss: 100, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationTitan] = new BlackOperation({ - name: BlackOperationName.OperationTitan, - baseDifficulty: 4000, - reqdRank: 10e3, - rankGain: 100, - rankLoss: 20, - hpLoss: 100, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationAres] = new BlackOperation({ - name: BlackOperationName.OperationAres, - baseDifficulty: 5000, - reqdRank: 12.5e3, - rankGain: 125, - rankLoss: 20, - hpLoss: 200, - weights: { - hack: 0, - str: 0.25, - def: 0.25, - dex: 0.25, - agi: 0.25, - cha: 0, - int: 0, - }, - decays: { - hack: 0, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationArchangel] = new BlackOperation({ - name: BlackOperationName.OperationArchangel, - baseDifficulty: 7500, - reqdRank: 15e3, - rankGain: 200, - rankLoss: 20, - hpLoss: 25, - weights: { - hack: 0, - str: 0.2, - def: 0.2, - dex: 0.3, - agi: 0.3, - cha: 0, - int: 0, - }, - decays: { - hack: 0, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationJuggernaut] = new BlackOperation({ - name: BlackOperationName.OperationJuggernaut, - baseDifficulty: 10e3, - reqdRank: 20e3, - rankGain: 300, - rankLoss: 40, - hpLoss: 300, - weights: { - hack: 0, - str: 0.25, - def: 0.25, - dex: 0.25, - agi: 0.25, - cha: 0, - int: 0, - }, - decays: { - hack: 0, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationRedDragon] = new BlackOperation({ - name: BlackOperationName.OperationRedDragon, - baseDifficulty: 12.5e3, - reqdRank: 25e3, - rankGain: 500, - rankLoss: 50, - hpLoss: 500, - weights: { - hack: 0.05, - str: 0.2, - def: 0.2, - dex: 0.25, - agi: 0.25, - cha: 0, - int: 0.05, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationK] = new BlackOperation({ - name: BlackOperationName.OperationK, - baseDifficulty: 15e3, - reqdRank: 30e3, - rankGain: 750, - rankLoss: 60, - hpLoss: 1000, - weights: { - hack: 0.05, - str: 0.2, - def: 0.2, - dex: 0.25, - agi: 0.25, - cha: 0, - int: 0.05, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationDeckard] = new BlackOperation({ - name: BlackOperationName.OperationDeckard, - baseDifficulty: 20e3, - reqdRank: 40e3, - rankGain: 1e3, - rankLoss: 75, - hpLoss: 200, - weights: { - hack: 0, - str: 0.24, - def: 0.24, - dex: 0.24, - agi: 0.24, - cha: 0, - int: 0.04, - }, - decays: { - hack: 0, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationTyrell] = new BlackOperation({ - name: BlackOperationName.OperationTyrell, - baseDifficulty: 25e3, - reqdRank: 50e3, - rankGain: 1.5e3, - rankLoss: 100, - hpLoss: 500, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationWallace] = new BlackOperation({ - name: BlackOperationName.OperationWallace, - baseDifficulty: 30e3, - reqdRank: 75e3, - rankGain: 2e3, - rankLoss: 150, - hpLoss: 1500, - weights: { - hack: 0, - str: 0.24, - def: 0.24, - dex: 0.24, - agi: 0.24, - cha: 0, - int: 0.04, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationShoulderOfOrion] = new BlackOperation({ - name: BlackOperationName.OperationShoulderOfOrion, - baseDifficulty: 35e3, - reqdRank: 100e3, - rankGain: 2.5e3, - rankLoss: 500, - hpLoss: 1500, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isStealth: true, - }); - BlackOperations[BlackOperationName.OperationHyron] = new BlackOperation({ - name: BlackOperationName.OperationHyron, - baseDifficulty: 40e3, - reqdRank: 125e3, - rankGain: 3e3, - rankLoss: 1e3, - hpLoss: 500, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationMorpheus] = new BlackOperation({ - name: BlackOperationName.OperationMorpheus, - baseDifficulty: 45e3, - reqdRank: 150e3, - rankGain: 4e3, - rankLoss: 1e3, - hpLoss: 100, - weights: { - hack: 0.05, - str: 0.15, - def: 0.15, - dex: 0.3, - agi: 0.3, - cha: 0, - int: 0.05, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isStealth: true, - }); - BlackOperations[BlackOperationName.OperationIonStorm] = new BlackOperation({ - name: BlackOperationName.OperationIonStorm, - baseDifficulty: 50e3, - reqdRank: 175e3, - rankGain: 5e3, - rankLoss: 1e3, - hpLoss: 5000, - weights: { - hack: 0, - str: 0.24, - def: 0.24, - dex: 0.24, - agi: 0.24, - cha: 0, - int: 0.04, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationAnnihilus] = new BlackOperation({ - name: BlackOperationName.OperationAnnihilus, - baseDifficulty: 55e3, - reqdRank: 200e3, - rankGain: 7.5e3, - rankLoss: 1e3, - hpLoss: 10e3, - weights: { - hack: 0, - str: 0.24, - def: 0.24, - dex: 0.24, - agi: 0.24, - cha: 0, - int: 0.04, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationUltron] = new BlackOperation({ - name: BlackOperationName.OperationUltron, - baseDifficulty: 60e3, - reqdRank: 250e3, - rankGain: 10e3, - rankLoss: 2e3, - hpLoss: 10e3, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - isKill: true, - }); - BlackOperations[BlackOperationName.OperationCenturion] = new BlackOperation({ - name: BlackOperationName.OperationCenturion, - baseDifficulty: 70e3, - reqdRank: 300e3, - rankGain: 15e3, - rankLoss: 5e3, - hpLoss: 10e3, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - }); - BlackOperations[BlackOperationName.OperationVindictus] = new BlackOperation({ - name: BlackOperationName.OperationVindictus, - baseDifficulty: 75e3, - reqdRank: 350e3, - rankGain: 20e3, - rankLoss: 20e3, - hpLoss: 20e3, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - }); - BlackOperations[BlackOperationName.OperationDaedalus] = new BlackOperation({ - name: BlackOperationName.OperationDaedalus, - baseDifficulty: 80e3, - reqdRank: 400e3, - rankGain: 40e3, - rankLoss: 10e3, - hpLoss: 100e3, - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.75, - }, - }); -})(); diff --git a/src/Bladeburner/Bladeburner.ts b/src/Bladeburner/Bladeburner.ts new file mode 100644 index 000000000..43ef84294 --- /dev/null +++ b/src/Bladeburner/Bladeburner.ts @@ -0,0 +1,1435 @@ +import type { PromisePair } from "../Types/Promises"; +import type { BlackOperation, Contract, GeneralAction, Operation } from "./Actions"; +import type { ActionIdentifier, Action, Attempt } from "./Types"; +import type { Person } from "../PersonObjects/Person"; +import type { Skills as PersonSkills } from "../PersonObjects/Skills"; + +import { + AugmentationName, + BladeActionType, + BladeContractName, + BladeGeneralActionName, + BladeMultName, + BladeOperationName, + BladeSkillName, + CityName, + FactionName, +} from "@enums"; +import { getKeyList } from "../utils/helpers/getKeyList"; +import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; +import { formatNumberNoSuffix } from "../ui/formatNumber"; +import { Skills } from "./data/Skills"; +import { City } from "./City"; +import { Player } from "@player"; +import { Router } from "../ui/GameRoot"; +import { ConsoleHelpText } from "./data/Help"; +import { exceptionAlert } from "../utils/helpers/exceptionAlert"; +import { getRandomInt } from "../utils/helpers/getRandomInt"; +import { BladeburnerConstants } from "./data/Constants"; +import { formatExp, formatMoney, formatPercent, formatBigNumber, formatStamina } from "../ui/formatNumber"; +import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; +import { addOffset } from "../utils/helpers/addOffset"; +import { Factions } from "../Faction/Factions"; +import { calculateHospitalizationCost } from "../Hospital/Hospital"; +import { dialogBoxCreate } from "../ui/React/DialogBox"; +import { Settings } from "../Settings/Settings"; +import { getTimestamp } from "../utils/helpers/getTimestamp"; +import { joinFaction } from "../Faction/FactionHelpers"; +import { isSleeveInfiltrateWork } from "../PersonObjects/Sleeve/Work/SleeveInfiltrateWork"; +import { isSleeveSupportWork } from "../PersonObjects/Sleeve/Work/SleeveSupportWork"; +import { WorkStats, newWorkStats } from "../Work/WorkStats"; +import { getEnumHelper } from "../utils/EnumHelper"; +import { PartialRecord, createEnumKeyedRecord, getRecordEntries } from "../Types/Record"; +import { createContracts, loadContractsData } from "./data/Contracts"; +import { createOperations, loadOperationsData } from "./data/Operations"; +import { clampInteger, clampNumber } from "../utils/helpers/clampNumber"; +import { parseCommand } from "../Terminal/Parser"; +import { BlackOperations } from "./data/BlackOperations"; +import { GeneralActions } from "./data/GeneralActions"; + +export const BladeburnerPromise: PromisePair = { promise: null, resolve: null }; + +export class Bladeburner { + numHosp = 0; + moneyLost = 0; + rank = 0; + maxRank = 0; + + skillPoints = 0; + totalSkillPoints = 0; + + teamSize = 0; + sleeveSize = 0; + teamLost = 0; + + storedCycles = 0; + + randomEventCounter: number = getRandomInt(240, 600); + + actionTimeToComplete = 0; + actionTimeCurrent = 0; + actionTimeOverflow = 0; + + action: ActionIdentifier | null = null; + + cities = createEnumKeyedRecord(CityName, (name) => new City(name)); + city = CityName.Sector12; + // Todo: better types for all these Record types. Will need custom types or enums for the named string categories (e.g. skills). + skills: PartialRecord = {}; + skillMultipliers: PartialRecord = {}; + staminaBonus = 0; + maxStamina = 1; + stamina = 1; + // Contracts and operations are stored on the Bladeburner object even though they are global so that they can utilize save/load of the main bladeburner object + contracts: Record; + operations: Record; + numBlackOpsComplete = 0; + logging = { + general: true, + contracts: true, + ops: true, + blackops: true, + events: true, + }; + automateEnabled = false; + automateActionHigh: ActionIdentifier | null = null; + automateThreshHigh = 0; + automateActionLow: ActionIdentifier | null = null; + automateThreshLow = 0; + consoleHistory: string[] = []; + consoleLogs: string[] = ["Bladeburner Console", "Type 'help' to see console commands"]; + + constructor() { + this.contracts = createContracts(); + this.operations = createOperations(); + } + + // Initialization code that is dependent on Player is here instead of in the constructor + init() { + this.calculateMaxStamina(); + this.stamina = this.maxStamina; + } + + getCurrentCity(): City { + return this.cities[this.city]; + } + + calculateStaminaPenalty(): number { + if (this.stamina === this.maxStamina) return 1; + return Math.min(1, this.stamina / (0.5 * this.maxStamina)); + } + + /** This function is for the player. Sleeves use their own functions to perform blade work. + * Note that this function does not ensure the action is valid, that should be checked before starting */ + startAction(actionId: ActionIdentifier | null): Attempt<{ message: string }> { + if (!actionId) { + this.resetAction(); + return { success: true, message: "Stopped current bladeburner action" }; + } + if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true)) Player.finishWork(true); + const action = this.getActionObject(actionId); + // This switch statement is just for handling error cases, it does not have to be exhaustive + const availability = action.getAvailability(this); + if (!availability.available) { + return { message: `Could not start action ${action.name}: ${availability.error}` }; + } + this.action = actionId; + this.actionTimeCurrent = 0; + this.actionTimeToComplete = action.getActionTime(this, Player); + return { success: true, message: `Started action ${action.name}` }; + } + + /** Directly sets a skill level, with no validation */ + setSkillLevel(skillName: BladeSkillName, value: number) { + this.skills[skillName] = clampInteger(value, 0); + this.updateSkillMultipliers(); + } + + /** Attempts to perform a skill upgrade, gives a message on both success and failure */ + upgradeSkill(skillName: BladeSkillName, count = 1): Attempt<{ message: string }> { + const availability = Skills[skillName].canUpgrade(this, count); + if (!availability.available) return { message: `Cannot upgrade ${skillName}: ${availability.error}` }; + this.skillPoints -= availability.cost; + this.setSkillLevel(skillName, (this.skills[skillName] ?? 0) + count); + return { success: true, message: `Upgraded skill ${skillName} by ${count} level${count > 1 ? "s" : ""}` }; + } + + executeConsoleCommands(commands: string): void { + try { + // Console History + if (this.consoleHistory[this.consoleHistory.length - 1] != commands) { + this.consoleHistory.push(commands); + if (this.consoleHistory.length > 50) { + this.consoleHistory.splice(0, 1); + } + } + + const arrayOfCommands = commands.split(";"); + for (let i = 0; i < arrayOfCommands.length; ++i) { + this.executeConsoleCommand(arrayOfCommands[i]); + } + } catch (e: unknown) { + exceptionAlert(e); + } + } + + postToConsole(input: string, saveToLogs = true): void { + const MaxConsoleEntries = 100; + if (saveToLogs) { + this.consoleLogs.push(input); + if (this.consoleLogs.length > MaxConsoleEntries) { + this.consoleLogs.shift(); + } + } + } + + log(input: string): void { + // Adds a timestamp and then just calls postToConsole + this.postToConsole(`[${getTimestamp()}] ${input}`); + } + + resetAction(): void { + this.action = null; + this.actionTimeCurrent = 0; + this.actionTimeToComplete = 0; + } + + clearConsole(): void { + this.consoleLogs.length = 0; + } + + prestigeAugmentation(): void { + this.resetAction(); + // Attempt to join the faction, this will silently fail if we have insufficient rank + this.joinFaction(); + } + + joinFaction(): Attempt<{ message: string }> { + const faction = Factions[FactionName.Bladeburners]; + if (faction.isMember) return { success: true, message: `Already a member of ${FactionName.Bladeburners} faction` }; + if (this.rank >= BladeburnerConstants.RankNeededForFaction) { + joinFaction(faction); + return { success: true, message: `Joined ${FactionName.Bladeburners} faction` }; + } + return { message: `Insufficient rank (${this.rank} / ${BladeburnerConstants.RankNeededForFaction})` }; + } + + storeCycles(numCycles = 0): void { + this.storedCycles = clampInteger(this.storedCycles + numCycles, 0); + } + + executeStartConsoleCommand(args: string[]): void { + if (args.length !== 3) { + this.postToConsole("Invalid usage of 'start' console command: start [type] [name]"); + this.postToConsole("Use 'help start' for more info"); + return; + } + const type = args[1]; + const name = args[2]; + const action = this.getActionFromTypeAndName(type, name); + if (!action) { + this.postToConsole(`Invalid action type / name specified: type: ${type}, name: ${name}`); + return; + } + const attempt = this.startAction(action.id); + this.postToConsole(attempt.message); + } + + getSkillMultsDisplay(): string[] { + const display: string[] = []; + for (const [multName, mult] of getRecordEntries(this.skillMultipliers)) { + display.push(`${multName}: x${formatNumberNoSuffix(mult, 3)}`); + } + return display; + } + + 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: "); + for (const skill of Object.values(Skills)) { + const skillLevel = this.getSkillLevel(skill.name); + this.postToConsole(`${skill.name}: Level ${formatNumberNoSuffix(skillLevel, 0)}\n\nEffects: `); + } + for (const logEntry of this.getSkillMultsDisplay()) this.postToConsole(logEntry); + } 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]; + if (!getEnumHelper("BladeSkillName").isMember(skillName)) { + this.postToConsole("Invalid skill name (Note that it is case-sensitive): " + skillName); + return; + } + const level = this.getSkillLevel(skillName); + if (args[1].toLowerCase() === "list") { + this.postToConsole(skillName + ": Level " + formatNumberNoSuffix(level)); + } else if (args[1].toLowerCase() === "level") { + const attempt = this.upgradeSkill(skillName); + this.postToConsole(attempt.message); + } else { + this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]"); + this.postToConsole("Use 'help skill' for more info"); + } + break; + } + default: { + this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]"); + this.postToConsole("Use 'help skill' for more info"); + break; + } + } + } + + executeLogConsoleCommand(args: string[]): void { + if (args.length < 3) { + this.postToConsole("Invalid usage of log command: log [enable/disable] [action/event]"); + this.postToConsole("Use 'help log' for more details and examples"); + return; + } + + let flag = true; + if (args[1].toLowerCase().includes("d")) { + flag = false; + } // d for disable + + switch (args[2].toLowerCase()) { + case "general": + case "gen": + this.logging.general = flag; + this.log("Logging " + (flag ? "enabled" : "disabled") + " for general actions"); + break; + case "contract": + case "contracts": + this.logging.contracts = flag; + this.log("Logging " + (flag ? "enabled" : "disabled") + " for Contracts"); + break; + case "ops": + case "op": + case "operations": + case "operation": + this.logging.ops = flag; + this.log("Logging " + (flag ? "enabled" : "disabled") + " for Operations"); + break; + case "blackops": + case "blackop": + case "black operations": + case "black operation": + this.logging.blackops = flag; + this.log("Logging " + (flag ? "enabled" : "disabled") + " for BlackOps"); + break; + case "event": + case "events": + this.logging.events = flag; + this.log("Logging " + (flag ? "enabled" : "disabled") + " for events"); + break; + case "all": + this.logging.general = flag; + this.logging.contracts = flag; + this.logging.ops = flag; + this.logging.blackops = flag; + this.logging.events = flag; + this.log("Logging " + (flag ? "enabled" : "disabled") + " for everything"); + break; + default: + this.postToConsole("Invalid action/event type specified: " + args[2]); + this.postToConsole( + "Examples of valid action/event identifiers are: [general, contracts, ops, blackops, events]", + ); + break; + } + } + + executeHelpConsoleCommand(args: string[]): void { + if (args.length === 1) { + for (const line of ConsoleHelpText.helpList) { + this.postToConsole(line); + } + } else { + for (let i = 1; i < args.length; ++i) { + if (!(args[i] in ConsoleHelpText)) continue; + const helpText = ConsoleHelpText[args[i]]; + for (const line of helpText) { + this.postToConsole(line); + } + } + } + } + + executeAutomateConsoleCommand(args: string[]): void { + if (args.length !== 2 && args.length !== 4) { + this.postToConsole( + "Invalid use of 'automate' command: automate [var] [val] [hi/low]. Use 'help automate' for more info", + ); + return; + } + + // Enable/Disable + if (args.length === 2) { + const flag = args[1]; + if (flag.toLowerCase() === "status") { + this.postToConsole("Automation: " + (this.automateEnabled ? "enabled" : "disabled")); + this.postToConsole( + "When your stamina drops to " + + formatNumberNoSuffix(this.automateThreshLow, 0) + + ", you will automatically switch to " + + (this.automateActionLow?.name ?? "Idle") + + ". When your stamina recovers to " + + formatNumberNoSuffix(this.automateThreshHigh, 0) + + ", you will automatically " + + "switch to " + + (this.automateActionHigh?.name ?? "Idle") + + ".", + ); + } else if (flag.toLowerCase().includes("en")) { + if (!this.automateActionLow || !this.automateActionHigh) { + return this.log("Failed to enable automation. Actions were not set"); + } + this.automateEnabled = true; + this.log("Bladeburner automation enabled"); + } else if (flag.toLowerCase().includes("d")) { + this.automateEnabled = false; + this.log("Bladeburner automation disabled"); + } else { + this.log("Invalid argument for 'automate' console command: " + args[1]); + } + return; + } + + // Set variables + if (args.length === 4) { + const type = args[1].toLowerCase(); // allows Action Type to be with or without capitalization. + const name = args[2]; + + let highLow = false; // True for high, false for low + if (args[3].toLowerCase().includes("hi")) { + highLow = true; + } + + let actionId: ActionIdentifier; + switch (type) { + case "stamina": + // For stamina, the "name" variable is actually the stamina threshold + if (isNaN(parseFloat(name))) { + this.postToConsole("Invalid value specified for stamina threshold (must be numeric): " + name); + } else { + if (highLow) { + this.automateThreshHigh = Number(name); + } else { + this.automateThreshLow = Number(name); + } + this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") stamina threshold set to " + name); + } + return; + case "general": + case "gen": { + if (!getEnumHelper("BladeGeneralActionName").isMember(name)) { + this.postToConsole("Invalid General Action name specified: " + name); + return; + } + actionId = { type: BladeActionType.general, name }; + break; + } + case "contract": + case "contracts": { + if (!getEnumHelper("BladeContractName").isMember(name)) { + this.postToConsole("Invalid Contract name specified: " + name); + return; + } + actionId = { type: BladeActionType.contract, name }; + break; + } + case "ops": + case "op": + case "operations": + case "operation": + if (!getEnumHelper("BladeOperationName").isMember(name)) { + this.postToConsole("Invalid Operation name specified: " + name); + return; + } + actionId = { type: BladeActionType.operation, name }; + break; + default: + this.postToConsole("Invalid use of automate command."); + return; + } + if (highLow) { + this.automateActionHigh = actionId; + } else { + this.automateActionLow = actionId; + } + this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + name); + + return; + } + } + + executeConsoleCommand(command: string): void { + command = command.trim(); + command = command.replace(/\s\s+/g, " "); // Replace all whitespace w/ a single space + + const args = parseCommand(command).map(String); + if (args.length <= 0) return; // Log an error? + + switch (args[0].toLowerCase()) { + case "automate": + this.executeAutomateConsoleCommand(args); + break; + case "clear": + case "cls": + this.clearConsole(); + break; + case "help": + this.executeHelpConsoleCommand(args); + break; + case "log": + this.executeLogConsoleCommand(args); + break; + case "skill": + this.executeSkillConsoleCommand(args); + break; + case "start": + this.executeStartConsoleCommand(args); + break; + case "stop": + this.resetAction(); + break; + default: + this.postToConsole("Invalid console command"); + break; + } + } + + triggerMigration(sourceCityName: CityName): void { + const cityHelper = getEnumHelper("CityName"); + let destCityName = cityHelper.random(); + while (destCityName === sourceCityName) destCityName = cityHelper.random(); + + const destCity = this.cities[destCityName]; + const sourceCity = this.cities[sourceCityName]; + + const rand = Math.random(); + let percentage = getRandomInt(3, 15) / 100; + + if (rand < 0.05 && sourceCity.comms > 0) { + // 5% chance for community migration + percentage *= getRandomInt(2, 4); // Migration increases population change + --sourceCity.comms; + ++destCity.comms; + } + const count = Math.round(sourceCity.pop * percentage); + sourceCity.pop -= count; + destCity.pop += count; + if (destCity.pop < BladeburnerConstants.PopGrowthCeiling) { + destCity.pop += BladeburnerConstants.BasePopGrowth; + } + } + + triggerPotentialMigration(sourceCityName: CityName, chance: number): void { + if (chance == null || isNaN(chance)) { + console.error("Invalid 'chance' parameter passed into Bladeburner.triggerPotentialMigration()"); + } + if (chance > 1) { + chance /= 100; + } + if (Math.random() < chance) { + this.triggerMigration(sourceCityName); + } + } + + randomEvent(): void { + const chance = Math.random(); + const cityHelper = getEnumHelper("CityName"); + + // Choose random source/destination city for events + const sourceCityName = cityHelper.random(); + const sourceCity = this.cities[sourceCityName]; + + let destCityName = cityHelper.random(); + while (destCityName === sourceCityName) destCityName = cityHelper.random(); + const destCity = this.cities[destCityName]; + + if (chance <= 0.05) { + // New Synthoid Community, 5% + ++sourceCity.comms; + const percentage = getRandomInt(10, 20) / 100; + const count = Math.round(sourceCity.pop * percentage); + sourceCity.pop += count; + if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) { + sourceCity.pop += BladeburnerConstants.BasePopGrowth; + } + if (this.logging.events) { + this.log("Intelligence indicates that a new Synthoid community was formed in a city"); + } + } else if (chance <= 0.1) { + // Synthoid Community Migration, 5% + if (sourceCity.comms <= 0) { + // If no comms in source city, then instead trigger a new Synthoid community event + ++sourceCity.comms; + const percentage = getRandomInt(10, 20) / 100; + const count = Math.round(sourceCity.pop * percentage); + sourceCity.pop += count; + if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) { + sourceCity.pop += BladeburnerConstants.BasePopGrowth; + } + if (this.logging.events) { + this.log("Intelligence indicates that a new Synthoid community was formed in a city"); + } + } else { + --sourceCity.comms; + ++destCity.comms; + + // Change pop + const percentage = getRandomInt(10, 20) / 100; + const count = Math.round(sourceCity.pop * percentage); + sourceCity.pop -= count; + destCity.pop += count; + if (destCity.pop < BladeburnerConstants.PopGrowthCeiling) { + destCity.pop += BladeburnerConstants.BasePopGrowth; + } + if (this.logging.events) { + this.log( + "Intelligence indicates that a Synthoid community migrated from " + sourceCityName + " to some other city", + ); + } + } + } else if (chance <= 0.3) { + // New Synthoids (non community), 20% + const percentage = getRandomInt(8, 24) / 100; + const count = Math.round(sourceCity.pop * percentage); + sourceCity.pop += count; + if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) { + sourceCity.pop += BladeburnerConstants.BasePopGrowth; + } + if (this.logging.events) { + this.log( + "Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly", + ); + } + } else if (chance <= 0.5) { + // Synthoid migration (non community) 20% + this.triggerMigration(sourceCityName); + if (this.logging.events) { + this.log( + "Intelligence indicates that a large number of Synthoids migrated from " + + sourceCityName + + " to some other city", + ); + } + } else if (chance <= 0.7) { + // Synthoid Riots (+chaos), 20% + sourceCity.chaos += 1; + sourceCity.chaos *= 1 + getRandomInt(5, 20) / 100; + if (this.logging.events) { + this.log("Tensions between Synthoids and humans lead to riots in " + sourceCityName + "! Chaos increased"); + } + } else if (chance <= 0.9) { + // Less Synthoids, 20% + const percentage = getRandomInt(8, 20) / 100; + const count = Math.round(sourceCity.pop * percentage); + sourceCity.pop -= count; + if (this.logging.events) { + this.log( + "Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly", + ); + } + } + // 10% chance of nothing happening + } + + /** + * Return stat to be gained from Contracts, Operations, and Black Operations + * @param action(Action obj) - Derived action class + * @param success(bool) - Whether action was successful + */ + getActionStats(action: Action, person: Person, success: boolean): WorkStats { + const difficulty = action.getDifficulty(); + + /** + * Gain multiplier based on difficulty. If it changes then the + * same variable calculated in completeAction() needs to change too + */ + const difficultyMult = + Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + + difficulty / BladeburnerConstants.DiffMultLinearFactor; + + const time = action.getActionTime(this, person); + const successMult = success ? 1 : 0.5; + + const unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult; + const unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult; + const skillMult = this.getSkillMult(BladeMultName.expGain); + + return { + hackExp: unweightedGain * action.weights.hacking * skillMult, + strExp: unweightedGain * action.weights.strength * skillMult, + defExp: unweightedGain * action.weights.defense * skillMult, + dexExp: unweightedGain * action.weights.dexterity * skillMult, + agiExp: unweightedGain * action.weights.agility * skillMult, + chaExp: unweightedGain * action.weights.charisma * skillMult, + intExp: unweightedIntGain * action.weights.intelligence * skillMult, + money: 0, + reputation: 0, + }; + } + + getDiplomacyEffectiveness(person: Person): number { + // Returns a decimal by which the city's chaos level should be multiplied (e.g. 0.98) + const CharismaLinearFactor = 1e3; + const CharismaExponentialFactor = 0.045; + + const charismaEff = + Math.pow(person.skills.charisma, CharismaExponentialFactor) + person.skills.charisma / CharismaLinearFactor; + return (100 - charismaEff) / 100; + } + + getRecruitmentSuccessChance(person: Person): number { + return Math.pow(person.skills.charisma, 0.45) / (this.teamSize - this.sleeveSize + 1); + } + + sleeveSupport(joining: boolean): void { + if (joining) { + this.sleeveSize += 1; + this.teamSize += 1; + } else { + this.sleeveSize -= 1; + this.teamSize -= 1; + } + } + + getSkillMult(name: BladeMultName): number { + return this.skillMultipliers[name] ?? 1; + } + + getEffectiveSkillLevel(person: Person, name: keyof PersonSkills): number { + switch (name) { + case "strength": + return person.skills.strength * this.getSkillMult(BladeMultName.effStr); + case "defense": + return person.skills.defense * this.getSkillMult(BladeMultName.effDef); + case "dexterity": + return person.skills.dexterity * this.getSkillMult(BladeMultName.effDex); + case "agility": + return person.skills.agility * this.getSkillMult(BladeMultName.effAgi); + case "charisma": + return person.skills.charisma * this.getSkillMult(BladeMultName.effCha); + default: + return person.skills[name]; + } + } + + updateSkillMultipliers(): void { + this.skillMultipliers = {}; + for (const skill of Object.values(Skills)) { + const level = this.getSkillLevel(skill.name); + if (!level) continue; + for (const [name, mult] of getRecordEntries(skill.mults)) { + this.skillMultipliers[name] = clampNumber(this.getSkillMult(name) * (1 + mult / 100), 0); + } + } + } + + completeOperation(success: boolean): void { + if (this.action?.type !== BladeActionType.operation) { + throw new Error("completeOperation() called even though current action is not an Operation"); + } + const action = this.getActionObject(this.action); + + // Calculate team losses + const teamCount = action.teamCount; + if (teamCount >= 1) { + const maxLosses = success ? Math.ceil(teamCount / 2) : Math.floor(teamCount); + const losses = getRandomInt(0, maxLosses); + this.teamSize -= losses; + if (this.teamSize < this.sleeveSize) { + const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork)); + for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { + const r = Math.floor(Math.random() * sup.length); + sup[r].takeDamage(sup[r].hp.max); + sup.splice(r, 1); + } + this.teamSize += this.sleeveSize; + } + this.teamLost += losses; + if (this.logging.ops && losses > 0) { + this.log("Lost " + formatNumberNoSuffix(losses, 0) + " team members during this " + action.name); + } + } + + const city = this.getCurrentCity(); + switch (action.name) { + case BladeOperationName.investigation: + if (success) { + city.improvePopulationEstimateByPercentage(0.4 * this.getSkillMult(BladeMultName.successChanceEstimate)); + } else { + this.triggerPotentialMigration(this.city, 0.1); + } + break; + case BladeOperationName.undercover: + if (success) { + city.improvePopulationEstimateByPercentage(0.8 * this.getSkillMult(BladeMultName.successChanceEstimate)); + } else { + this.triggerPotentialMigration(this.city, 0.15); + } + break; + case BladeOperationName.sting: + if (success) { + city.changePopulationByPercentage(-0.1, { + changeEstEqually: true, + nonZero: true, + }); + } + city.changeChaosByCount(0.1); + break; + case BladeOperationName.raid: + if (success) { + city.changePopulationByPercentage(-1, { + changeEstEqually: true, + nonZero: true, + }); + --city.comms; + } else { + const change = getRandomInt(-10, -5) / 10; + city.changePopulationByPercentage(change, { + nonZero: true, + changeEstEqually: false, + }); + } + city.changeChaosByPercentage(getRandomInt(1, 5)); + break; + case BladeOperationName.stealthRetirement: + if (success) { + city.changePopulationByPercentage(-0.5, { + changeEstEqually: true, + nonZero: true, + }); + } + city.changeChaosByPercentage(getRandomInt(-3, -1)); + break; + case BladeOperationName.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); + } + } + + completeContract(success: boolean, action: Contract): void { + const city = this.getCurrentCity(); + if (success) { + switch (action.name) { + case BladeContractName.tracking: + // Increase estimate accuracy by a relatively small amount + city.improvePopulationEstimateByCount( + getRandomInt(100, 1e3) * this.getSkillMult(BladeMultName.successChanceEstimate), + ); + break; + case BladeContractName.bountyHunter: + city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 }); + city.changeChaosByCount(0.02); + break; + case BladeContractName.retirement: + city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 }); + city.changeChaosByCount(0.04); + break; + } + } + } + + completeAction(person: Person, actionIdent: ActionIdentifier, isPlayer = true): WorkStats { + let retValue = newWorkStats(); + const action = this.getActionObject(actionIdent); + switch (action.type) { + case BladeActionType.contract: + case BladeActionType.operation: { + try { + const isOperation = action.type === BladeActionType.operation; + const difficulty = action.getDifficulty(); + const difficultyMultiplier = + Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + + difficulty / BladeburnerConstants.DiffMultLinearFactor; + const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1); + + if (isPlayer) { + // Stamina loss is based on difficulty + this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; + if (this.stamina < 0) { + this.stamina = 0; + } + } + + // Process Contract/Operation success/failure + if (action.attempt(this, person)) { + retValue = this.getActionStats(action, person, true); + ++action.successes; + --action.count; + + // Earn money for contracts + let moneyGain = 0; + if (!isOperation) { + moneyGain = + BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.getSkillMult(BladeMultName.money); + retValue.money = moneyGain; + } + + if (isOperation) { + action.setMaxLevel(BladeburnerConstants.OperationSuccessesPerLevel); + } else { + action.setMaxLevel(BladeburnerConstants.ContractSuccessesPerLevel); + } + if (action.rankGain) { + const gain = addOffset(action.rankGain * rewardMultiplier * currentNodeMults.BladeburnerRank, 10); + this.changeRank(person, gain); + if (isOperation && this.logging.ops) { + this.log( + `${person.whoAmI()}: ${action.name} successfully completed! Gained ${formatBigNumber(gain)} rank`, + ); + } else if (!isOperation && this.logging.contracts) { + this.log( + `${person.whoAmI()}: ${action.name} contract successfully completed! Gained ` + + `${formatBigNumber(gain)} rank and ${formatMoney(moneyGain)}`, + ); + } + } + isOperation ? this.completeOperation(true) : this.completeContract(true, action); + } else { + retValue = this.getActionStats(action, person, false); + ++action.failures; + --action.count; + let loss = 0, + damage = 0; + if (action.rankLoss) { + loss = addOffset(action.rankLoss * rewardMultiplier, 10); + this.changeRank(person, -1 * loss); + } + if (action.hpLoss) { + damage = action.hpLoss * difficultyMultiplier; + damage = Math.ceil(addOffset(damage, 10)); + const cost = calculateHospitalizationCost(damage); + if (person.takeDamage(damage)) { + ++this.numHosp; + this.moneyLost += cost; + } + } + let logLossText = ""; + if (loss > 0) { + logLossText += "Lost " + formatNumberNoSuffix(loss, 3) + " rank. "; + } + if (damage > 0) { + logLossText += "Took " + formatNumberNoSuffix(damage, 0) + " damage."; + } + if (isOperation && this.logging.ops) { + this.log(`${person.whoAmI()}: ` + action.name + " failed! " + logLossText); + } else if (!isOperation && this.logging.contracts) { + this.log(`${person.whoAmI()}: ` + action.name + " contract failed! " + logLossText); + } + isOperation ? this.completeOperation(false) : this.completeContract(false, action); + } + if (action.autoLevel) { + action.level = action.maxLevel; + } // Autolevel + } catch (e: unknown) { + exceptionAlert(e); + } + break; + } + case BladeActionType.blackOp: { + const difficulty = action.getDifficulty(); + const difficultyMultiplier = + Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + + difficulty / BladeburnerConstants.DiffMultLinearFactor; + + // Stamina loss is based on difficulty + this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; + if (this.stamina < 0) { + this.stamina = 0; + } + + // Team loss variables + const teamCount = action.teamCount; + let teamLossMax; + + if (action.attempt(this, person)) { + retValue = this.getActionStats(action, person, true); + this.numBlackOpsComplete++; + let rankGain = 0; + if (action.rankGain) { + rankGain = addOffset(action.rankGain * currentNodeMults.BladeburnerRank, 10); + this.changeRank(person, rankGain); + } + teamLossMax = Math.ceil(teamCount / 2); + + if (this.logging.blackops) { + this.log(`${person.whoAmI()}: ${action.name} successful! Gained ${formatNumberNoSuffix(rankGain, 1)} rank`); + } + } else { + retValue = this.getActionStats(action, person, false); + let rankLoss = 0; + let damage = 0; + if (action.rankLoss) { + rankLoss = addOffset(action.rankLoss, 10); + this.changeRank(person, -1 * rankLoss); + } + if (action.hpLoss) { + damage = action.hpLoss * difficultyMultiplier; + damage = Math.ceil(addOffset(damage, 10)); + const cost = calculateHospitalizationCost(damage); + if (person.takeDamage(damage)) { + ++this.numHosp; + this.moneyLost += cost; + } + } + teamLossMax = Math.floor(teamCount); + + if (this.logging.blackops) { + this.log( + `${person.whoAmI()}: ${action.name} failed! Lost ${formatNumberNoSuffix( + rankLoss, + 1, + )} rank and took ${formatNumberNoSuffix(damage, 0)} damage`, + ); + } + } + + this.resetAction(); // Stop regardless of success or fail + + // Calculate team losses + if (teamCount >= 1) { + const losses = getRandomInt(1, teamLossMax); + this.teamSize -= losses; + if (this.teamSize < this.sleeveSize) { + const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork)); + for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { + const r = Math.floor(Math.random() * sup.length); + sup[r].takeDamage(sup[r].hp.max); + sup.splice(r, 1); + } + this.teamSize += this.sleeveSize; + } + this.teamLost += losses; + if (this.logging.blackops) { + this.log( + `${person.whoAmI()}: You lost ${formatNumberNoSuffix(losses, 0)} team members during ${action.name}`, + ); + } + } + break; + } + case BladeActionType.general: + switch (action.name) { + case BladeGeneralActionName.training: { + this.stamina -= 0.5 * BladeburnerConstants.BaseStaminaLoss; + const strExpGain = 30 * person.mults.strength_exp, + defExpGain = 30 * person.mults.defense_exp, + dexExpGain = 30 * person.mults.dexterity_exp, + agiExpGain = 30 * person.mults.agility_exp, + staminaGain = 0.04 * this.getSkillMult(BladeMultName.stamina); + retValue.strExp = strExpGain; + retValue.defExp = defExpGain; + retValue.dexExp = dexExpGain; + retValue.agiExp = agiExpGain; + this.staminaBonus += staminaGain; + if (this.logging.general) { + this.log( + `${person.whoAmI()}: ` + + "Training completed. Gained: " + + formatExp(strExpGain) + + " str exp, " + + formatExp(defExpGain) + + " def exp, " + + formatExp(dexExpGain) + + " dex exp, " + + formatExp(agiExpGain) + + " agi exp, " + + formatBigNumber(staminaGain) + + " max stamina", + ); + } + break; + } + case BladeGeneralActionName.fieldAnalysis: { + // Does not use stamina. Effectiveness depends on hacking, int, and cha + let eff = + 0.04 * Math.pow(person.skills.hacking, 0.3) + + 0.04 * Math.pow(person.skills.intelligence, 0.9) + + 0.02 * Math.pow(person.skills.charisma, 0.3); + eff *= person.mults.bladeburner_analysis; + if (isNaN(eff) || eff < 0) { + throw new Error("Field Analysis Effectiveness calculated to be NaN or negative"); + } + const hackingExpGain = 20 * person.mults.hacking_exp; + const charismaExpGain = 20 * person.mults.charisma_exp; + const rankGain = 0.1 * currentNodeMults.BladeburnerRank; + retValue.hackExp = hackingExpGain; + retValue.chaExp = charismaExpGain; + retValue.intExp = BladeburnerConstants.BaseIntGain; + this.changeRank(person, rankGain); + this.getCurrentCity().improvePopulationEstimateByPercentage( + eff * this.getSkillMult(BladeMultName.successChanceEstimate), + ); + if (this.logging.general) { + this.log( + `${person.whoAmI()}: ` + + `Field analysis completed. Gained ${formatBigNumber(rankGain)} rank, ` + + `${formatExp(hackingExpGain)} hacking exp, and ` + + `${formatExp(charismaExpGain)} charisma exp`, + ); + } + break; + } + case BladeGeneralActionName.recruitment: { + const actionTime = action.getActionTime(this, person) * 1000; + if (action.attempt(this, person)) { + const expGain = 2 * BladeburnerConstants.BaseStatGain * actionTime; + retValue.chaExp = expGain; + ++this.teamSize; + if (this.logging.general) { + this.log( + `${person.whoAmI()}: ` + + "Successfully recruited a team member! Gained " + + formatExp(expGain) + + " charisma exp", + ); + } + } else { + const expGain = BladeburnerConstants.BaseStatGain * actionTime; + retValue.chaExp = expGain; + if (this.logging.general) { + this.log( + `${person.whoAmI()}: ` + + "Failed to recruit a team member. Gained " + + formatExp(expGain) + + " charisma exp", + ); + } + } + break; + } + case BladeGeneralActionName.diplomacy: { + const eff = this.getDiplomacyEffectiveness(person); + this.getCurrentCity().chaos *= eff; + if (this.getCurrentCity().chaos < 0) { + this.getCurrentCity().chaos = 0; + } + if (this.logging.general) { + this.log( + `${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${formatPercent( + 1 - eff, + )}`, + ); + } + break; + } + case BladeGeneralActionName.hyperbolicRegen: { + person.regenerateHp(BladeburnerConstants.HrcHpGain); + + const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100); + this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain); + if (this.logging.general) { + this.log( + `${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${ + BladeburnerConstants.HrcHpGain + } HP and gained ${formatStamina(staminaGain)} stamina`, + ); + } + break; + } + case BladeGeneralActionName.inciteViolence: { + for (const contract of Object.values(this.contracts)) { + contract.count += (60 * 3 * contract.growthFunction()) / BladeburnerConstants.ActionCountGrowthPeriod; + } + for (const operation of Object.values(this.operations)) { + operation.count += (60 * 3 * operation.growthFunction()) / BladeburnerConstants.ActionCountGrowthPeriod; + } + if (this.logging.general) { + this.log(`${person.whoAmI()}: Incited violence in the synthoid communities.`); + } + for (const cityName of Object.values(CityName)) { + const city = this.cities[cityName]; + city.chaos += 10; + city.chaos += city.chaos / (Math.log(city.chaos) / Math.log(10)); + } + break; + } + default: { + // Verify general actions switch statement is exhaustive + const __a: never = action; + } + } + break; + default: { + // Verify type switch statement is exhaustive + const __a: never = action; + } + } + return retValue; + } + + infiltrateSynthoidCommunities(): void { + const infilSleeves = Player.sleeves.filter((s) => isSleeveInfiltrateWork(s.currentWork)).length; + const amt = Math.pow(infilSleeves, -0.5) / 2; + for (const contract of Object.values(BladeContractName)) { + this.contracts[contract].count += amt; + } + for (const operation of Object.values(BladeOperationName)) { + this.operations[operation].count += amt; + } + if (this.logging.general) { + this.log(`Sleeve: Infiltrate the synthoid communities.`); + } + } + + changeRank(person: Person, change: number): void { + if (isNaN(change)) { + throw new Error("NaN passed into Bladeburner.changeRank()"); + } + this.rank += change; + if (this.rank < 0) { + this.rank = 0; + } + this.maxRank = Math.max(this.rank, this.maxRank); + + const bladeburnersFactionName = FactionName.Bladeburners; + const bladeburnerFac = Factions[bladeburnersFactionName]; + if (bladeburnerFac.isMember) { + const favorBonus = 1 + bladeburnerFac.favor / 100; + bladeburnerFac.playerReputation += + BladeburnerConstants.RankToFactionRepFactor * change * person.mults.faction_rep * favorBonus; + } + + // Gain skill points + const rankNeededForSp = (this.totalSkillPoints + 1) * BladeburnerConstants.RanksPerSkillPoint; + if (this.maxRank >= rankNeededForSp) { + // Calculate how many skill points to gain + const gainedSkillPoints = Math.floor( + (this.maxRank - rankNeededForSp) / BladeburnerConstants.RanksPerSkillPoint + 1, + ); + this.skillPoints += gainedSkillPoints; + this.totalSkillPoints += gainedSkillPoints; + } + } + + processAction(seconds: number): void { + // Store action to avoid losing reference to it is action is reset during this function + if (!this.action) return; // Idle + const action = this.getActionObject(this.action); + // If the action is no longer valid, discontinue the action + if (!action.getAvailability(this).available) return this.resetAction(); + + // If the previous action went past its completion time, add to the next action + // This is not added immediately in case the automation changes the action + this.actionTimeCurrent += seconds + this.actionTimeOverflow; + this.actionTimeOverflow = 0; + // Complete the task if it's complete + if (this.actionTimeCurrent >= this.actionTimeToComplete) { + this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete; + const retValue = this.completeAction(Player, action.id); + Player.gainMoney(retValue.money, "bladeburner"); + Player.gainStats(retValue); + if (action.type != BladeActionType.blackOp) { + this.startAction(action.id); // Attempt to repeat action + } + } + } + + calculateStaminaGainPerSecond(): number { + const effAgility = this.getEffectiveSkillLevel(Player, "agility"); + const maxStaminaBonus = this.maxStamina / BladeburnerConstants.MaxStaminaToGainFactor; + const gain = (BladeburnerConstants.StaminaGainPerSecond + maxStaminaBonus) * Math.pow(effAgility, 0.17); + return clampNumber(gain * (this.getSkillMult(BladeMultName.stamina) * Player.mults.bladeburner_stamina_gain), 0); + } + + calculateMaxStamina(): void { + const baseStamina = Math.pow(this.getEffectiveSkillLevel(Player, "agility"), 0.8); + const maxStamina = clampNumber( + (baseStamina + this.staminaBonus) * + this.getSkillMult(BladeMultName.stamina) * + Player.mults.bladeburner_max_stamina, + 0, + ); + if (this.maxStamina === maxStamina) return; + // If max stamina changed, adjust stamina accordingly + const oldMax = this.maxStamina; + this.maxStamina = maxStamina; + this.stamina = clampNumber((this.maxStamina * this.stamina) / oldMax, 0, maxStamina); + } + + getSkillLevel(skillName: BladeSkillName): number { + return this.skills[skillName] ?? 0; + } + + process(): void { + // Edge race condition when the engine checks the processing counters and attempts to route before the router is initialized. + if (!Router.isInitialized) return; + + // If the Player starts doing some other actions, set action to idle and alert + if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true) && Player.currentWork) { + if (this.action) { + let msg = "Your Bladeburner action was cancelled because you started doing something else."; + if (this.automateEnabled) { + msg += `\n\nYour automation was disabled as well. You will have to re-enable it through the Bladeburner console`; + this.automateEnabled = false; + } + if (!Settings.SuppressBladeburnerPopup) { + dialogBoxCreate(msg); + } + } + this.resetAction(); + } + + // If the Player has no Stamina, set action to idle + if (this.stamina <= 0) { + this.log("Your Bladeburner action was cancelled because your stamina hit 0"); + this.resetAction(); + } + + // A 'tick' for this mechanic is one second (= 5 game cycles) + if (this.storedCycles >= BladeburnerConstants.CyclesPerSecond) { + let seconds = Math.floor(this.storedCycles / BladeburnerConstants.CyclesPerSecond); + seconds = Math.min(seconds, 5); // Max of 5 'ticks' + this.storedCycles -= seconds * BladeburnerConstants.CyclesPerSecond; + + // Stamina + this.calculateMaxStamina(); + this.stamina += this.calculateStaminaGainPerSecond() * seconds; + this.stamina = Math.min(this.maxStamina, this.stamina); + + // Count increase for contracts/operations + for (const contract of Object.values(this.contracts)) { + contract.count += (seconds * contract.growthFunction()) / BladeburnerConstants.ActionCountGrowthPeriod; + } + for (const op of Object.values(this.operations)) { + op.count += (seconds * op.growthFunction()) / BladeburnerConstants.ActionCountGrowthPeriod; + } + + // Chaos goes down very slowly + for (const cityName of Object.values(CityName)) { + const city = this.cities[cityName]; + if (!city) throw new Error("Invalid city when processing passive chaos reduction in Bladeburner.process"); + city.chaos -= 0.0001 * seconds; + city.chaos = Math.max(0, city.chaos); + } + + // Random Events + this.randomEventCounter -= seconds; + if (this.randomEventCounter <= 0) { + this.randomEvent(); + // Add instead of setting because we might have gone over the required time for the event + this.randomEventCounter += getRandomInt(240, 600); + } + + this.processAction(seconds); + + // Automation + if (this.automateEnabled) { + // Note: Do NOT set this.action = this.automateActionHigh/Low since it creates a reference + if (this.stamina <= this.automateThreshLow && this.action?.name !== this.automateActionLow?.name) { + this.startAction(this.automateActionLow); + } else if (this.stamina >= this.automateThreshHigh && this.action?.name !== this.automateActionHigh?.name) { + this.startAction(this.automateActionHigh); + } + } + + // Handle "nextUpdate" resolver after this update + if (BladeburnerPromise.resolve) { + BladeburnerPromise.resolve(seconds * 1000); + BladeburnerPromise.resolve = null; + BladeburnerPromise.promise = null; + } + } + } + + /** Return the action based on an ActionIdentifier, discriminating types when possible */ + getActionObject(actionId: ActionIdentifier & { type: BladeActionType.blackOp }): BlackOperation; + getActionObject(actionId: ActionIdentifier & { type: BladeActionType.operation }): Operation; + getActionObject(actionId: ActionIdentifier & { type: BladeActionType.contract }): Contract; + getActionObject(actionId: ActionIdentifier & { type: BladeActionType.general }): GeneralAction; + getActionObject(actionId: ActionIdentifier): Action; + getActionObject(actionId: ActionIdentifier): Action { + switch (actionId.type) { + case BladeActionType.contract: + return this.contracts[actionId.name]; + case BladeActionType.operation: + return this.operations[actionId.name]; + case BladeActionType.blackOp: + return BlackOperations[actionId.name]; + case BladeActionType.general: + return GeneralActions[actionId.name]; + } + } + + /** Fuzzy matching for action identifiers. Should be removed in 3.0 */ + getActionFromTypeAndName(type: string, name: string): Action | null { + if (!type || !name) return null; + const convertedType = type.toLowerCase().trim(); + switch (convertedType) { + case "contract": + case "contracts": + case "contr": + if (!getEnumHelper("BladeContractName").isMember(name)) return null; + return this.contracts[name]; + case "operation": + case "operations": + case "op": + case "ops": + if (!getEnumHelper("BladeOperationName").isMember(name)) return null; + return this.operations[name]; + case "blackoperation": + case "black operation": + case "black operations": + case "black op": + case "black ops": + case "blackop": + case "blackops": + if (!getEnumHelper("BladeBlackOpName").isMember(name)) return null; + return BlackOperations[name]; + case "general": + case "general action": + case "gen": { + if (!getEnumHelper("BladeGeneralActionName").isMember(name)) return null; + return GeneralActions[name]; + } + } + return null; + } + + static keysToSave = getKeyList(Bladeburner, { removedKeys: ["skillMultipliers"] }); + // Don't load contracts or operations because of the special loading method they use, see fromJSON + static keysToLoad = getKeyList(Bladeburner, { removedKeys: ["skillMultipliers", "contracts", "operations"] }); + + /** Serialize the current object to a JSON save state. */ + toJSON(): IReviverValue { + return Generic_toJSON("Bladeburner", this, Bladeburner.keysToSave); + } + + /** Initializes a Bladeburner object from a JSON save state. */ + static fromJSON(value: IReviverValue): Bladeburner { + // operations and contracts are not loaded directly from the save, we load them in using a different method + const contractsData = value.data?.contracts; + const operationsData = value.data?.operations; + const bladeburner = Generic_fromJSON(Bladeburner, value.data, Bladeburner.keysToLoad); + // Loading this way allows better typesafety and also allows faithfully reconstructing contracts/operations + // even from save data that is missing a lot of static info about the objects. + loadContractsData(contractsData, bladeburner.contracts); + loadOperationsData(operationsData, bladeburner.operations); + // Regenerate skill multiplier data, which is not included in savedata + bladeburner.updateSkillMultipliers(); + return bladeburner; + } +} + +constructorsForReviver.Bladeburner = Bladeburner; diff --git a/src/Bladeburner/Bladeburner.tsx b/src/Bladeburner/Bladeburner.tsx deleted file mode 100644 index 355d3b2d5..000000000 --- a/src/Bladeburner/Bladeburner.tsx +++ /dev/null @@ -1,2454 +0,0 @@ -import type { PromisePair } from "../Types/Promises"; -import { AugmentationName, CityName, FactionName } from "@enums"; -import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; -import { ActionIdentifier } from "./ActionIdentifier"; -import { ActionTypes } from "./data/ActionTypes"; -import { Growths } from "./data/Growths"; -import { BlackOperations } from "./BlackOperations"; -import { BlackOperation } from "./BlackOperation"; -import { Operation } from "./Operation"; -import { Contract } from "./Contract"; -import { GeneralActions } from "./GeneralActions"; -import { formatNumberNoSuffix } from "../ui/formatNumber"; -import { Skills } from "./Skills"; -import { Skill } from "./Skill"; -import { City } from "./City"; -import { Action } from "./Action"; -import { Player } from "@player"; -import { Person } from "../PersonObjects/Person"; -import { Router } from "../ui/GameRoot"; -import { ConsoleHelpText } from "./data/Help"; -import { exceptionAlert } from "../utils/helpers/exceptionAlert"; -import { getRandomInt } from "../utils/helpers/getRandomInt"; -import { BladeburnerConstants } from "./data/Constants"; -import { formatExp, formatMoney, formatPercent, formatBigNumber, formatStamina } from "../ui/formatNumber"; -import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; -import { addOffset } from "../utils/helpers/addOffset"; -import { Factions } from "../Faction/Factions"; -import { calculateHospitalizationCost } from "../Hospital/Hospital"; -import { dialogBoxCreate } from "../ui/React/DialogBox"; -import { Settings } from "../Settings/Settings"; -import { getTimestamp } from "../utils/helpers/getTimestamp"; -import { joinFaction } from "../Faction/FactionHelpers"; -import { WorkerScript } from "../Netscript/WorkerScript"; -import { KEY } from "../utils/helpers/keyCodes"; -import { isSleeveInfiltrateWork } from "../PersonObjects/Sleeve/Work/SleeveInfiltrateWork"; -import { isSleeveSupportWork } from "../PersonObjects/Sleeve/Work/SleeveSupportWork"; -import { WorkStats, newWorkStats } from "../Work/WorkStats"; -import { getEnumHelper } from "../utils/EnumHelper"; -import { createEnumKeyedRecord } from "../Types/Record"; - -export interface BlackOpsAttempt { - error?: string; - isAvailable?: boolean; - action?: BlackOperation; -} -export const BladeburnerPromise: PromisePair = { promise: null, resolve: null }; - -export class Bladeburner { - numHosp = 0; - moneyLost = 0; - rank = 0; - maxRank = 0; - - skillPoints = 0; - totalSkillPoints = 0; - - teamSize = 0; - sleeveSize = 0; - teamLost = 0; - hpLost = 0; - - storedCycles = 0; - - randomEventCounter: number = getRandomInt(240, 600); - - actionTimeToComplete = 0; - actionTimeCurrent = 0; - actionTimeOverflow = 0; - - action: ActionIdentifier = new ActionIdentifier({ - type: ActionTypes.Idle, - }); - - cities = createEnumKeyedRecord(CityName, (name) => new City(name)); - city = CityName.Sector12; - // Todo: better types for all these Record types. Will need custom types or enums for the named string categories (e.g. skills). - skills: Record = {}; - skillMultipliers: Record = {}; - staminaBonus = 0; - maxStamina = 0; - stamina = 0; - contracts: Record = {}; - operations: Record = {}; - blackops: Record = {}; - logging = { - general: true, - contracts: true, - ops: true, - blackops: true, - events: true, - }; - automateEnabled = false; - automateActionHigh: ActionIdentifier = new ActionIdentifier({ - type: ActionTypes.Idle, - }); - automateThreshHigh = 0; - automateActionLow: ActionIdentifier = new ActionIdentifier({ - type: ActionTypes.Idle, - }); - automateThreshLow = 0; - consoleHistory: string[] = []; - consoleLogs: string[] = ["Bladeburner Console", "Type 'help' to see console commands"]; - - constructor() { - this.updateSkillMultipliers(); // Calls resetSkillMultipliers() - - // Max Stamina is based on stats and Bladeburner-specific bonuses - this.calculateMaxStamina(); - this.stamina = this.maxStamina; - this.create(); - } - /* - just a quick fix for the broken implementation - BlackOperations are only initialized on game load with a count of 1 - and are not reset on BitNode change or dev menu reset of bladeburner - */ - resetBlackOps(): void { - for (const [blackopName, blackop] of Object.entries(BlackOperations)) { - blackop.count = Number(!this.blackops[blackopName]); - } - } - - getCurrentCity(): City { - return this.cities[this.city]; - } - - calculateStaminaPenalty(): number { - if (this.stamina === this.maxStamina) return 1; - return Math.min(1, this.stamina / (0.5 * this.maxStamina)); - } - - // Todo, deduplicate this functionality - getNextBlackOp(): { name: string; rank: number } | null { - let blackops: BlackOperation[] = []; - for (const blackopName of Object.keys(BlackOperations)) { - if (Object.hasOwn(BlackOperations, blackopName)) { - blackops.push(BlackOperations[blackopName]); - } - } - blackops.sort(function (a, b) { - return a.reqdRank - b.reqdRank; - }); - - blackops = blackops.filter( - (blackop: BlackOperation, i: number) => - !(this.blackops[blackops[i].name] == null && i !== 0 && this.blackops[blackops[i - 1].name] == null), - ); - - blackops = blackops.reverse(); - const actionID = this.getActionIdFromTypeAndName("Black Op", "Operation Daedalus"); - - return blackops[0].name === "Operation Daedalus" && - actionID !== null && - !this.canAttemptBlackOp(actionID).isAvailable - ? null - : { name: blackops[0].name, rank: blackops[0].reqdRank }; - } - - canAttemptBlackOp(actionId: ActionIdentifier): BlackOpsAttempt { - // Safety measure - don't repeat BlackOps that are already done - if (this.blackops[actionId.name] != null) { - return { error: "Tried to start a Black Operation that had already been completed" }; - } - - const action = this.getActionObject(actionId); - if (!(action instanceof BlackOperation)) throw new Error(`Action should be BlackOperation but isn't`); - if (action == null) throw new Error("Failed to get BlackOperation object for: " + actionId.name); - - if (action.reqdRank > this.rank) { - return { error: "Tried to start a Black Operation without the rank requirement" }; - } - - // Can't start a BlackOp if you haven't done the one before it - const blackops = []; - for (const nm of Object.keys(BlackOperations)) { - if (Object.hasOwn(BlackOperations, nm)) { - blackops.push(nm); - } - } - blackops.sort(function (a, b) { - return BlackOperations[a].reqdRank - BlackOperations[b].reqdRank; // Sort black ops in intended order - }); - - const i = blackops.indexOf(actionId.name); - if (i === -1) { - return { error: `Invalid Black Op: '${name}'` }; - } - - if (i > 0 && this.blackops[blackops[i - 1]] == null) { - return { error: `Preceding Black Op must be completed before starting '${actionId.name}'.` }; - } - - return { isAvailable: true, action }; - } - - /** This function is only for the player. Sleeves use their own functions to perform blade work. - * Todo: partial unification of player and sleeve methods? */ - startAction(actionId: ActionIdentifier): void { - if (actionId == null) return; - this.action = actionId; - this.actionTimeCurrent = 0; - switch (actionId.type) { - case ActionTypes.Idle: - this.actionTimeToComplete = 0; - break; - case ActionTypes.Contract: - try { - const action = this.getActionObject(actionId); - if (action == null) { - throw new Error("Failed to get Contract Object for: " + actionId.name); - } - if (action.count < 1) { - return this.resetAction(); - } - this.actionTimeToComplete = action.getActionTime(this, Player); - } catch (e: unknown) { - exceptionAlert(e); - } - break; - case ActionTypes.Operation: { - try { - const action = this.getActionObject(actionId); - if (action == null) { - throw new Error("Failed to get Operation Object for: " + actionId.name); - } - if (action.count < 1) { - return this.resetAction(); - } - if (actionId.name === "Raid" && this.getCurrentCity().comms === 0) { - return this.resetAction(); - } - this.actionTimeToComplete = action.getActionTime(this, Player); - } catch (e: unknown) { - exceptionAlert(e); - } - break; - } - case ActionTypes.BlackOp: - case ActionTypes.BlackOperation: { - try { - const testBlackOp = this.canAttemptBlackOp(actionId); - if (!testBlackOp.isAvailable) { - this.resetAction(); - this.log(`Error: ${testBlackOp.error}`); - break; - } - if (testBlackOp.action === undefined) { - throw new Error("action should not be null"); - } - this.actionTimeToComplete = testBlackOp.action.getActionTime(this, Player); - } catch (e: unknown) { - exceptionAlert(e); - } - break; - } - case ActionTypes.Recruitment: - this.actionTimeToComplete = this.getRecruitmentTime(Player); - break; - case ActionTypes.Training: - case ActionTypes.FieldAnalysis: - case ActionTypes["Field Analysis"]: - this.actionTimeToComplete = 30; - break; - case ActionTypes.Diplomacy: - case ActionTypes["Hyperbolic Regeneration Chamber"]: - case ActionTypes["Incite Violence"]: - this.actionTimeToComplete = 60; - break; - default: - throw new Error("Invalid Action Type in bladeburner.startAction(): " + actionId.type); - } - } - - upgradeSkill(skill: Skill, count = 1): void { - // This does NOT handle deduction of skill points - const skillName = skill.name; - if (this.skills[skillName]) { - this.skills[skillName] += count; - } else { - this.skills[skillName] = count; - } - if (isNaN(this.skills[skillName]) || this.skills[skillName] < 0) { - throw new Error("Level of Skill " + skillName + " is invalid: " + this.skills[skillName]); - } - this.updateSkillMultipliers(); - } - - executeConsoleCommands(commands: string): void { - try { - // Console History - if (this.consoleHistory[this.consoleHistory.length - 1] != commands) { - this.consoleHistory.push(commands); - if (this.consoleHistory.length > 50) { - this.consoleHistory.splice(0, 1); - } - } - - const arrayOfCommands = commands.split(";"); - for (let i = 0; i < arrayOfCommands.length; ++i) { - this.executeConsoleCommand(arrayOfCommands[i]); - } - } catch (e: unknown) { - exceptionAlert(e); - } - } - - postToConsole(input: string, saveToLogs = true): void { - const MaxConsoleEntries = 100; - if (saveToLogs) { - this.consoleLogs.push(input); - if (this.consoleLogs.length > MaxConsoleEntries) { - this.consoleLogs.shift(); - } - } - } - - log(input: string): void { - // Adds a timestamp and then just calls postToConsole - this.postToConsole(`[${getTimestamp()}] ${input}`); - } - - resetAction(): void { - this.action = new ActionIdentifier({ type: ActionTypes.Idle }); - this.actionTimeCurrent = 0; - this.actionTimeToComplete = 0; - } - - clearConsole(): void { - this.consoleLogs.length = 0; - } - - prestige(): void { - this.resetAction(); - const bladeburnerFac = Factions[FactionName.Bladeburners]; - if (this.rank >= BladeburnerConstants.RankNeededForFaction) { - joinFaction(bladeburnerFac); - } - } - - storeCycles(numCycles = 0): void { - this.storedCycles += numCycles; - } - - getActionIdFromTypeAndName(type = "", name = ""): ActionIdentifier | null { - if (type === "" || name === "") { - return null; - } - const action = new ActionIdentifier(); - const convertedType = type.toLowerCase().trim(); - const convertedName = name.toLowerCase().trim(); - switch (convertedType) { - case "contract": - case "contracts": - case "contr": - action.type = ActionTypes.Contract; - if (Object.hasOwn(this.contracts, name)) { - action.name = name; - return action; - } - return null; - case "operation": - case "operations": - case "op": - case "ops": - action.type = ActionTypes.Operation; - if (Object.hasOwn(this.operations, name)) { - action.name = name; - return action; - } - return null; - case "blackoperation": - case "black operation": - case "black operations": - case "black op": - case "black ops": - case "blackop": - case "blackops": - action.type = ActionTypes.BlackOp; - if (Object.hasOwn(BlackOperations, name)) { - action.name = name; - return action; - } - return null; - case "general": - case "general action": - case "gen": - break; - default: - return null; - } - - if (convertedType.startsWith("gen")) { - switch (convertedName) { - case "training": - action.type = ActionTypes.Training; - action.name = "Training"; - break; - case "recruitment": - case "recruit": - action.type = ActionTypes.Recruitment; - action.name = "Recruitment"; - break; - case "field analysis": - case "fieldanalysis": - action.type = ActionTypes["Field Analysis"]; - action.name = "Field Analysis"; - break; - case "diplomacy": - action.type = ActionTypes.Diplomacy; - action.name = "Diplomacy"; - break; - case "hyperbolic regeneration chamber": - action.type = ActionTypes["Hyperbolic Regeneration Chamber"]; - action.name = "Hyperbolic Regeneration Chamber"; - break; - case "incite violence": - action.type = ActionTypes["Incite Violence"]; - action.name = "Incite Violence"; - break; - default: - return null; - } - return action; - } - - return null; - } - - executeStartConsoleCommand(args: string[]): void { - if (args.length !== 3) { - this.postToConsole("Invalid usage of 'start' console command: start [type] [name]"); - this.postToConsole("Use 'help start' for more info"); - return; - } - const name = args[2]; - switch (args[1].toLowerCase()) { - case "general": - case "gen": - if (GeneralActions[name] != null) { - this.action.type = ActionTypes[name]; - this.action.name = name; - this.startAction(this.action); - } else { - this.postToConsole("Invalid action name specified: " + args[2]); - } - break; - case "contract": - case "contracts": - if (this.contracts[name] != null) { - this.action.type = ActionTypes.Contract; - this.action.name = name; - this.startAction(this.action); - } else { - this.postToConsole("Invalid contract name specified: " + args[2]); - } - break; - case "ops": - case "op": - case "operations": - case "operation": - if (this.operations[name] != null) { - this.action.type = ActionTypes.Operation; - this.action.name = name; - this.startAction(this.action); - } else { - this.postToConsole("Invalid Operation name specified: " + args[2]); - } - break; - case "blackops": - case "blackop": - case "black operations": - case "black operation": - if (BlackOperations[name] != null) { - this.action.type = ActionTypes.BlackOperation; - this.action.name = name; - this.startAction(this.action); - } else { - this.postToConsole("Invalid BlackOp name specified: " + args[2]); - } - break; - default: - this.postToConsole("Invalid action/event type specified: " + args[1]); - this.postToConsole("Examples of valid action/event identifiers are: [general, contract, op, blackop]"); - break; - } - } - - executeSkillConsoleCommand(args: string[]): void { - switch (args.length) { - case 1: { - // Display Skill Help Command - this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]"); - this.postToConsole("Use 'help skill' for more info"); - break; - } - case 2: { - if (args[1].toLowerCase() === "list") { - // List all skills and their level - this.postToConsole("Skills: "); - const skillNames = Object.keys(Skills); - for (let i = 0; i < skillNames.length; ++i) { - const skill = Skills[skillNames[i]]; - let level = 0; - if (this.skills[skill.name] != null) { - level = this.skills[skill.name]; - } - this.postToConsole(skill.name + ": Level " + formatNumberNoSuffix(level, 0)); - } - this.postToConsole(" "); - this.postToConsole("Effects: "); - const multKeys = Object.keys(this.skillMultipliers); - for (let i = 0; i < multKeys.length; ++i) { - const mult = this.skillMultipliers[multKeys[i]]; - if (mult && mult !== 1) { - const mults = formatNumberNoSuffix(mult, 3); - switch (multKeys[i]) { - case "successChanceAll": - this.postToConsole("Total Success Chance: x" + mults); - break; - case "successChanceStealth": - this.postToConsole("Stealth Success Chance: x" + mults); - break; - case "successChanceKill": - this.postToConsole("Retirement Success Chance: x" + mults); - break; - case "successChanceContract": - this.postToConsole("Contract Success Chance: x" + mults); - break; - case "successChanceOperation": - this.postToConsole("Operation Success Chance: x" + mults); - break; - case "successChanceEstimate": - this.postToConsole("Synthoid Data Estimate: x" + mults); - break; - case "actionTime": - this.postToConsole("Action Time: x" + mults); - break; - case "effHack": - this.postToConsole("Hacking Skill: x" + mults); - break; - case "effStr": - this.postToConsole("Strength: x" + mults); - break; - case "effDef": - this.postToConsole("Defense: x" + mults); - break; - case "effDex": - this.postToConsole("Dexterity: x" + mults); - break; - case "effAgi": - this.postToConsole("Agility: x" + mults); - break; - case "effCha": - this.postToConsole("Charisma: x" + mults); - break; - case "effInt": - this.postToConsole("Intelligence: x" + mults); - break; - case "stamina": - this.postToConsole("Stamina: x" + mults); - break; - default: - console.warn(`Unrecognized SkillMult Key: ${multKeys[i]}`); - break; - } - } - } - } else { - this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]"); - this.postToConsole("Use 'help skill' for more info"); - } - break; - } - case 3: { - const skillName = args[2]; - const skill = Skills[skillName]; - if (!skill) { - this.postToConsole("Invalid skill name (Note that it is case-sensitive): " + skillName); - break; - } - if (args[1].toLowerCase() === "list") { - let level = 0; - if (this.skills[skill.name] !== undefined) { - level = this.skills[skill.name]; - } - this.postToConsole(skill.name + ": Level " + formatNumberNoSuffix(level)); - } else if (args[1].toLowerCase() === "level") { - let currentLevel = 0; - if (this.skills[skillName] && !isNaN(this.skills[skillName])) { - currentLevel = this.skills[skillName]; - } - const pointCost = skill.calculateCost(currentLevel); - if (skill.maxLvl !== 0 && currentLevel >= skill.maxLvl) { - this.postToConsole(`This skill ${skill.name} is already at max level (${currentLevel}/${skill.maxLvl}).`); - } else if (this.skillPoints >= pointCost) { - this.skillPoints -= pointCost; - this.upgradeSkill(skill); - this.log(skill.name + " upgraded to Level " + this.skills[skillName]); - } else { - this.postToConsole( - "You do not have enough Skill Points to upgrade this. You need " + formatNumberNoSuffix(pointCost, 0), - ); - } - } else { - this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]"); - this.postToConsole("Use 'help skill' for more info"); - } - break; - } - default: { - this.postToConsole("Invalid usage of 'skill' console command: skill [action] [name]"); - this.postToConsole("Use 'help skill' for more info"); - break; - } - } - } - - executeLogConsoleCommand(args: string[]): void { - if (args.length < 3) { - this.postToConsole("Invalid usage of log command: log [enable/disable] [action/event]"); - this.postToConsole("Use 'help log' for more details and examples"); - return; - } - - let flag = true; - if (args[1].toLowerCase().includes("d")) { - flag = false; - } // d for disable - - switch (args[2].toLowerCase()) { - case "general": - case "gen": - this.logging.general = flag; - this.log("Logging " + (flag ? "enabled" : "disabled") + " for general actions"); - break; - case "contract": - case "contracts": - this.logging.contracts = flag; - this.log("Logging " + (flag ? "enabled" : "disabled") + " for Contracts"); - break; - case "ops": - case "op": - case "operations": - case "operation": - this.logging.ops = flag; - this.log("Logging " + (flag ? "enabled" : "disabled") + " for Operations"); - break; - case "blackops": - case "blackop": - case "black operations": - case "black operation": - this.logging.blackops = flag; - this.log("Logging " + (flag ? "enabled" : "disabled") + " for BlackOps"); - break; - case "event": - case "events": - this.logging.events = flag; - this.log("Logging " + (flag ? "enabled" : "disabled") + " for events"); - break; - case "all": - this.logging.general = flag; - this.logging.contracts = flag; - this.logging.ops = flag; - this.logging.blackops = flag; - this.logging.events = flag; - this.log("Logging " + (flag ? "enabled" : "disabled") + " for everything"); - break; - default: - this.postToConsole("Invalid action/event type specified: " + args[2]); - this.postToConsole( - "Examples of valid action/event identifiers are: [general, contracts, ops, blackops, events]", - ); - break; - } - } - - executeHelpConsoleCommand(args: string[]): void { - if (args.length === 1) { - for (const line of ConsoleHelpText.helpList) { - this.postToConsole(line); - } - } else { - for (let i = 1; i < args.length; ++i) { - if (!(args[i] in ConsoleHelpText)) continue; - const helpText = ConsoleHelpText[args[i]]; - for (const line of helpText) { - this.postToConsole(line); - } - } - } - } - - executeAutomateConsoleCommand(args: string[]): void { - if (args.length !== 2 && args.length !== 4) { - this.postToConsole( - "Invalid use of 'automate' command: automate [var] [val] [hi/low]. Use 'help automate' for more info", - ); - return; - } - - // Enable/Disable - if (args.length === 2) { - const flag = args[1]; - if (flag.toLowerCase() === "status") { - this.postToConsole("Automation: " + (this.automateEnabled ? "enabled" : "disabled")); - this.postToConsole( - "When your stamina drops to " + - formatNumberNoSuffix(this.automateThreshLow, 0) + - ", you will automatically switch to " + - this.automateActionLow.name + - ". When your stamina recovers to " + - formatNumberNoSuffix(this.automateThreshHigh, 0) + - ", you will automatically " + - "switch to " + - this.automateActionHigh.name + - ".", - ); - } else if (flag.toLowerCase().includes("en")) { - if (!this.automateActionLow || !this.automateActionHigh) { - return this.log("Failed to enable automation. Actions were not set"); - } - this.automateEnabled = true; - this.log("Bladeburner automation enabled"); - } else if (flag.toLowerCase().includes("d")) { - this.automateEnabled = false; - this.log("Bladeburner automation disabled"); - } else { - this.log("Invalid argument for 'automate' console command: " + args[1]); - } - return; - } - - // Set variables - if (args.length === 4) { - const variable = args[1].toLowerCase(); // allows Action Type to be with or without capitalization. - const val = args[2]; - - let highLow = false; // True for high, false for low - if (args[3].toLowerCase().includes("hi")) { - highLow = true; - } - - switch (variable) { - case "general": - case "gen": - if (GeneralActions[val] != null) { - const action = new ActionIdentifier({ - type: ActionTypes[val], - name: val, - }); - if (highLow) { - this.automateActionHigh = action; - } else { - this.automateActionLow = action; - } - this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val); - } else { - this.postToConsole("Invalid action name specified: " + val); - } - break; - case "contract": - case "contracts": - if (this.contracts[val] != null) { - const action = new ActionIdentifier({ - type: ActionTypes.Contract, - name: val, - }); - if (highLow) { - this.automateActionHigh = action; - } else { - this.automateActionLow = action; - } - this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val); - } else { - this.postToConsole("Invalid contract name specified: " + val); - } - break; - case "ops": - case "op": - case "operations": - case "operation": - if (this.operations[val] != null) { - const action = new ActionIdentifier({ - type: ActionTypes.Operation, - name: val, - }); - if (highLow) { - this.automateActionHigh = action; - } else { - this.automateActionLow = action; - } - this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") action set to " + val); - } else { - this.postToConsole("Invalid Operation name specified: " + val); - } - break; - case "stamina": - if (isNaN(parseFloat(val))) { - this.postToConsole("Invalid value specified for stamina threshold (must be numeric): " + val); - } else { - if (highLow) { - this.automateThreshHigh = Number(val); - } else { - this.automateThreshLow = Number(val); - } - this.log("Automate (" + (highLow ? "HIGH" : "LOW") + ") stamina threshold set to " + val); - } - break; - default: - break; - } - - return; - } - } - - parseCommandArguments(command: string): string[] { - /** - * Returns an array with command and its arguments in each index. - * e.g. skill "blade's intuition" foo returns [skill, blade's intuition, foo] - * The input to the fn will be trimmed and will have all whitespace replaced w/ a single space - */ - const args = []; - let start = 0; - let i = 0; - while (i < command.length) { - const c = command.charAt(i); - if (c === '"' || c === "'") { - // Double quotes or Single quotes - const endQuote = command.indexOf(c, i + 1); - if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) { - args.push(command.substr(i + 1, endQuote - i - 1)); - if (endQuote === command.length - 1) { - start = i = endQuote + 1; - } else { - start = i = endQuote + 2; // Skip the space - } - continue; - } - } else if (c === KEY.SPACE) { - args.push(command.substr(start, i - start)); - start = i + 1; - } - ++i; - } - if (start !== i) { - args.push(command.substr(start, i - start)); - } - return args; - } - - executeConsoleCommand(command: string): void { - command = command.trim(); - command = command.replace(/\s\s+/g, " "); // Replace all whitespace w/ a single space - - const args = this.parseCommandArguments(command); - if (args.length <= 0) return; // Log an error? - - switch (args[0].toLowerCase()) { - case "automate": - this.executeAutomateConsoleCommand(args); - break; - case "clear": - case "cls": - this.clearConsole(); - break; - case "help": - this.executeHelpConsoleCommand(args); - break; - case "log": - this.executeLogConsoleCommand(args); - break; - case "skill": - this.executeSkillConsoleCommand(args); - break; - case "start": - this.executeStartConsoleCommand(args); - break; - case "stop": - this.resetAction(); - break; - default: - this.postToConsole("Invalid console command"); - break; - } - } - - triggerMigration(sourceCityName: CityName): void { - const cityHelper = getEnumHelper("CityName"); - let destCityName = cityHelper.random(); - while (destCityName === sourceCityName) destCityName = cityHelper.random(); - - const destCity = this.cities[destCityName]; - const sourceCity = this.cities[sourceCityName]; - - const rand = Math.random(); - let percentage = getRandomInt(3, 15) / 100; - - if (rand < 0.05 && sourceCity.comms > 0) { - // 5% chance for community migration - percentage *= getRandomInt(2, 4); // Migration increases population change - --sourceCity.comms; - ++destCity.comms; - } - const count = Math.round(sourceCity.pop * percentage); - sourceCity.pop -= count; - destCity.pop += count; - if (destCity.pop < BladeburnerConstants.PopGrowthCeiling) { - destCity.pop += BladeburnerConstants.BasePopGrowth; - } - } - - triggerPotentialMigration(sourceCityName: CityName, chance: number): void { - if (chance == null || isNaN(chance)) { - console.error("Invalid 'chance' parameter passed into Bladeburner.triggerPotentialMigration()"); - } - if (chance > 1) { - chance /= 100; - } - if (Math.random() < chance) { - this.triggerMigration(sourceCityName); - } - } - - randomEvent(): void { - const chance = Math.random(); - const cityHelper = getEnumHelper("CityName"); - - // Choose random source/destination city for events - const sourceCityName = cityHelper.random(); - const sourceCity = this.cities[sourceCityName]; - - let destCityName = cityHelper.random(); - while (destCityName === sourceCityName) destCityName = cityHelper.random(); - const destCity = this.cities[destCityName]; - - if (chance <= 0.05) { - // New Synthoid Community, 5% - ++sourceCity.comms; - const percentage = getRandomInt(10, 20) / 100; - const count = Math.round(sourceCity.pop * percentage); - sourceCity.pop += count; - if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) { - sourceCity.pop += BladeburnerConstants.BasePopGrowth; - } - if (this.logging.events) { - this.log("Intelligence indicates that a new Synthoid community was formed in a city"); - } - } else if (chance <= 0.1) { - // Synthoid Community Migration, 5% - if (sourceCity.comms <= 0) { - // If no comms in source city, then instead trigger a new Synthoid community event - ++sourceCity.comms; - const percentage = getRandomInt(10, 20) / 100; - const count = Math.round(sourceCity.pop * percentage); - sourceCity.pop += count; - if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) { - sourceCity.pop += BladeburnerConstants.BasePopGrowth; - } - if (this.logging.events) { - this.log("Intelligence indicates that a new Synthoid community was formed in a city"); - } - } else { - --sourceCity.comms; - ++destCity.comms; - - // Change pop - const percentage = getRandomInt(10, 20) / 100; - const count = Math.round(sourceCity.pop * percentage); - sourceCity.pop -= count; - destCity.pop += count; - if (destCity.pop < BladeburnerConstants.PopGrowthCeiling) { - destCity.pop += BladeburnerConstants.BasePopGrowth; - } - if (this.logging.events) { - this.log( - "Intelligence indicates that a Synthoid community migrated from " + sourceCityName + " to some other city", - ); - } - } - } else if (chance <= 0.3) { - // New Synthoids (non community), 20% - const percentage = getRandomInt(8, 24) / 100; - const count = Math.round(sourceCity.pop * percentage); - sourceCity.pop += count; - if (sourceCity.pop < BladeburnerConstants.PopGrowthCeiling) { - sourceCity.pop += BladeburnerConstants.BasePopGrowth; - } - if (this.logging.events) { - this.log( - "Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly", - ); - } - } else if (chance <= 0.5) { - // Synthoid migration (non community) 20% - this.triggerMigration(sourceCityName); - if (this.logging.events) { - this.log( - "Intelligence indicates that a large number of Synthoids migrated from " + - sourceCityName + - " to some other city", - ); - } - } else if (chance <= 0.7) { - // Synthoid Riots (+chaos), 20% - sourceCity.chaos += 1; - sourceCity.chaos *= 1 + getRandomInt(5, 20) / 100; - if (this.logging.events) { - this.log("Tensions between Synthoids and humans lead to riots in " + sourceCityName + "! Chaos increased"); - } - } else if (chance <= 0.9) { - // Less Synthoids, 20% - const percentage = getRandomInt(8, 20) / 100; - const count = Math.round(sourceCity.pop * percentage); - sourceCity.pop -= count; - if (this.logging.events) { - this.log( - "Intelligence indicates that the Synthoid population of " + sourceCityName + " just changed significantly", - ); - } - } - // 10% chance of nothing happening - } - - /** - * Return stat to be gained from Contracts, Operations, and Black Operations - * @param action(Action obj) - Derived action class - * @param success(bool) - Whether action was successful - */ - getActionStats(action: Action, person: Person, success: boolean): WorkStats { - const difficulty = action.getDifficulty(); - - /** - * Gain multiplier based on difficulty. If it changes then the - * same variable calculated in completeAction() needs to change too - */ - const difficultyMult = - Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + - difficulty / BladeburnerConstants.DiffMultLinearFactor; - - const time = action.getActionTime(this, person); - const successMult = success ? 1 : 0.5; - - const unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult; - const unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult; - const skillMult = this.skillMultipliers.expGain; - - return { - hackExp: unweightedGain * action.weights.hack * skillMult, - strExp: unweightedGain * action.weights.str * skillMult, - defExp: unweightedGain * action.weights.def * skillMult, - dexExp: unweightedGain * action.weights.dex * skillMult, - agiExp: unweightedGain * action.weights.agi * skillMult, - chaExp: unweightedGain * action.weights.cha * skillMult, - intExp: unweightedIntGain * action.weights.int * skillMult, - money: 0, - reputation: 0, - }; - } - - getDiplomacyEffectiveness(person: Person): number { - // Returns a decimal by which the city's chaos level should be multiplied (e.g. 0.98) - const CharismaLinearFactor = 1e3; - const CharismaExponentialFactor = 0.045; - - const charismaEff = - Math.pow(person.skills.charisma, CharismaExponentialFactor) + person.skills.charisma / CharismaLinearFactor; - return (100 - charismaEff) / 100; - } - - getRecruitmentSuccessChance(person: Person): number { - return Math.pow(person.skills.charisma, 0.45) / (this.teamSize - this.sleeveSize + 1); - } - - getRecruitmentTime(person: Person): number { - const effCharisma = person.skills.charisma * this.skillMultipliers.effCha; - const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90; - return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor)); - } - - sleeveSupport(joining: boolean): void { - if (joining) { - this.sleeveSize += 1; - this.teamSize += 1; - } else { - this.sleeveSize -= 1; - this.teamSize -= 1; - } - } - - resetSkillMultipliers(): void { - this.skillMultipliers = { - successChanceAll: 1, - successChanceStealth: 1, - successChanceKill: 1, - successChanceContract: 1, - successChanceOperation: 1, - successChanceEstimate: 1, - actionTime: 1, - effHack: 1, - effStr: 1, - effDef: 1, - effDex: 1, - effAgi: 1, - effCha: 1, - effInt: 1, - stamina: 1, - money: 1, - expGain: 1, - }; - } - - updateSkillMultipliers(): void { - this.resetSkillMultipliers(); - for (const skillName of Object.keys(this.skills)) { - if (Object.hasOwn(this.skills, skillName)) { - const skill = Skills[skillName]; - if (skill == null) { - throw new Error("Could not find Skill Object for: " + skillName); - } - const level = this.skills[skillName]; - if (level == null || level <= 0) { - continue; - } //Not upgraded - - const multiplierNames = Object.keys(this.skillMultipliers); - for (let i = 0; i < multiplierNames.length; ++i) { - const multiplierName = multiplierNames[i]; - if (skill.getMultiplier(multiplierName) != null && !isNaN(skill.getMultiplier(multiplierName))) { - const value = skill.getMultiplier(multiplierName) * level; - let multiplierValue = 1 + value / 100; - if (multiplierName === "actionTime") { - multiplierValue = 1 - value / 100; - } - this.skillMultipliers[multiplierName] *= multiplierValue; - } - } - } - } - } - - completeOperation(success: boolean): void { - if (this.action.type !== ActionTypes.Operation) { - throw new Error("completeOperation() called even though current action is not an Operation"); - } - const action = this.getActionObject(this.action); - if (action == null) { - throw new Error("Failed to get Contract/Operation Object for: " + this.action.name); - } - - // Calculate team losses - const teamCount = action.teamCount; - if (teamCount >= 1) { - let max; - if (success) { - max = Math.ceil(teamCount / 2); - } else { - max = Math.floor(teamCount); - } - const losses = getRandomInt(0, max); - this.teamSize -= losses; - if (this.teamSize < this.sleeveSize) { - const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork)); - for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { - const r = Math.floor(Math.random() * sup.length); - sup[r].takeDamage(sup[r].hp.max); - sup.splice(r, 1); - } - this.teamSize += this.sleeveSize; - } - this.teamLost += losses; - if (this.logging.ops && losses > 0) { - this.log("Lost " + formatNumberNoSuffix(losses, 0) + " team members during this " + action.name); - } - } - - const city = this.getCurrentCity(); - switch (action.name) { - case "Investigation": - if (success) { - city.improvePopulationEstimateByPercentage(0.4 * this.skillMultipliers.successChanceEstimate); - } else { - this.triggerPotentialMigration(this.city, 0.1); - } - break; - case "Undercover Operation": - if (success) { - city.improvePopulationEstimateByPercentage(0.8 * this.skillMultipliers.successChanceEstimate); - } else { - this.triggerPotentialMigration(this.city, 0.15); - } - break; - case "Sting Operation": - if (success) { - city.changePopulationByPercentage(-0.1, { - changeEstEqually: true, - nonZero: true, - }); - } - city.changeChaosByCount(0.1); - break; - case "Raid": - if (success) { - city.changePopulationByPercentage(-1, { - changeEstEqually: true, - nonZero: true, - }); - --city.comms; - } else { - const change = getRandomInt(-10, -5) / 10; - city.changePopulationByPercentage(change, { - nonZero: true, - changeEstEqually: false, - }); - } - city.changeChaosByPercentage(getRandomInt(1, 5)); - break; - case "Stealth Retirement Operation": - if (success) { - city.changePopulationByPercentage(-0.5, { - changeEstEqually: true, - nonZero: true, - }); - } - city.changeChaosByPercentage(getRandomInt(-3, -1)); - break; - case "Assassination": - if (success) { - city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 }); - } - city.changeChaosByPercentage(getRandomInt(-5, 5)); - break; - default: - throw new Error("Invalid Action name in completeOperation: " + this.action.name); - } - } - - getActionObject(actionId: ActionIdentifier): Action | null { - /** - * Given an ActionIdentifier object, returns the corresponding - * GeneralAction, Contract, Operation, or BlackOperation object - */ - switch (actionId.type) { - case ActionTypes.Contract: - return this.contracts[actionId.name]; - case ActionTypes.Operation: - return this.operations[actionId.name]; - case ActionTypes.BlackOp: - case ActionTypes.BlackOperation: - return BlackOperations[actionId.name]; - case ActionTypes.Training: - return GeneralActions.Training; - case ActionTypes["Field Analysis"]: - return GeneralActions["Field Analysis"]; - case ActionTypes.Recruitment: - return GeneralActions.Recruitment; - case ActionTypes.Diplomacy: - return GeneralActions.Diplomacy; - case ActionTypes["Hyperbolic Regeneration Chamber"]: - return GeneralActions["Hyperbolic Regeneration Chamber"]; - case ActionTypes["Incite Violence"]: - return GeneralActions["Incite Violence"]; - default: - return null; - } - } - - completeContract(success: boolean, actionIdent: ActionIdentifier): void { - if (actionIdent.type !== ActionTypes.Contract) { - throw new Error("completeContract() called even though current action is not a Contract"); - } - const city = this.getCurrentCity(); - if (success) { - switch (actionIdent.name) { - case "Tracking": - // Increase estimate accuracy by a relatively small amount - city.improvePopulationEstimateByCount(getRandomInt(100, 1e3) * this.skillMultipliers.successChanceEstimate); - break; - case "Bounty Hunter": - city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 }); - city.changeChaosByCount(0.02); - break; - case "Retirement": - city.changePopulationByCount(-1, { estChange: -1, estOffset: 0 }); - city.changeChaosByCount(0.04); - break; - default: - throw new Error("Invalid Action name in completeContract: " + actionIdent.name); - } - } - } - - completeAction(person: Person, actionIdent: ActionIdentifier, isPlayer = true): WorkStats { - let retValue = newWorkStats(); - switch (actionIdent.type) { - case ActionTypes.Contract: - case ActionTypes.Operation: { - try { - const isOperation = actionIdent.type === ActionTypes.Operation; - const action = this.getActionObject(actionIdent); - if (action == null) { - throw new Error("Failed to get Contract/Operation Object for: " + actionIdent.name); - } - const difficulty = action.getDifficulty(); - const difficultyMultiplier = - Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + - difficulty / BladeburnerConstants.DiffMultLinearFactor; - const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1); - - if (isPlayer) { - // Stamina loss is based on difficulty - this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; - if (this.stamina < 0) { - this.stamina = 0; - } - } - - // Process Contract/Operation success/failure - if (action.attempt(this, person)) { - retValue = this.getActionStats(action, person, true); - ++action.successes; - --action.count; - - // Earn money for contracts - let moneyGain = 0; - if (!isOperation) { - moneyGain = BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.skillMultipliers.money; - retValue.money = moneyGain; - } - - if (isOperation) { - action.setMaxLevel(BladeburnerConstants.OperationSuccessesPerLevel); - } else { - action.setMaxLevel(BladeburnerConstants.ContractSuccessesPerLevel); - } - if (action.rankGain) { - const gain = addOffset(action.rankGain * rewardMultiplier * currentNodeMults.BladeburnerRank, 10); - this.changeRank(person, gain); - if (isOperation && this.logging.ops) { - this.log( - `${person.whoAmI()}: ${action.name} successfully completed! Gained ${formatBigNumber(gain)} rank`, - ); - } else if (!isOperation && this.logging.contracts) { - this.log( - `${person.whoAmI()}: ${action.name} contract successfully completed! Gained ` + - `${formatBigNumber(gain)} rank and ${formatMoney(moneyGain)}`, - ); - } - } - isOperation ? this.completeOperation(true) : this.completeContract(true, actionIdent); - } else { - retValue = this.getActionStats(action, person, false); - ++action.failures; - --action.count; - let loss = 0, - damage = 0; - if (action.rankLoss) { - loss = addOffset(action.rankLoss * rewardMultiplier, 10); - this.changeRank(person, -1 * loss); - } - if (action.hpLoss) { - damage = action.hpLoss * difficultyMultiplier; - damage = Math.ceil(addOffset(damage, 10)); - this.hpLost += damage; - const cost = calculateHospitalizationCost(damage); - if (person.takeDamage(damage)) { - ++this.numHosp; - this.moneyLost += cost; - } - } - let logLossText = ""; - if (loss > 0) { - logLossText += "Lost " + formatNumberNoSuffix(loss, 3) + " rank. "; - } - if (damage > 0) { - logLossText += "Took " + formatNumberNoSuffix(damage, 0) + " damage."; - } - if (isOperation && this.logging.ops) { - this.log(`${person.whoAmI()}: ` + action.name + " failed! " + logLossText); - } else if (!isOperation && this.logging.contracts) { - this.log(`${person.whoAmI()}: ` + action.name + " contract failed! " + logLossText); - } - isOperation ? this.completeOperation(false) : this.completeContract(false, actionIdent); - } - if (action.autoLevel) { - action.level = action.maxLevel; - } // Autolevel - } catch (e: unknown) { - exceptionAlert(e); - } - break; - } - case ActionTypes.BlackOp: - case ActionTypes.BlackOperation: { - try { - const action = this.getActionObject(actionIdent); - if (action == null || !(action instanceof BlackOperation)) { - throw new Error("Failed to get BlackOperation Object for: " + actionIdent.name); - } - const difficulty = action.getDifficulty(); - const difficultyMultiplier = - Math.pow(difficulty, BladeburnerConstants.DiffMultExponentialFactor) + - difficulty / BladeburnerConstants.DiffMultLinearFactor; - - // Stamina loss is based on difficulty - this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; - if (this.stamina < 0) { - this.stamina = 0; - } - - // Team loss variables - const teamCount = action.teamCount; - let teamLossMax; - - if (action.attempt(this, person)) { - retValue = this.getActionStats(action, person, true); - action.count = 0; - this.blackops[action.name] = true; - let rankGain = 0; - if (action.rankGain) { - rankGain = addOffset(action.rankGain * currentNodeMults.BladeburnerRank, 10); - this.changeRank(person, rankGain); - } - teamLossMax = Math.ceil(teamCount / 2); - - if (this.logging.blackops) { - this.log( - `${person.whoAmI()}: ` + - action.name + - " successful! Gained " + - formatNumberNoSuffix(rankGain, 1) + - " rank", - ); - } - } else { - retValue = this.getActionStats(action, person, false); - let rankLoss = 0; - let damage = 0; - if (action.rankLoss) { - rankLoss = addOffset(action.rankLoss, 10); - this.changeRank(person, -1 * rankLoss); - } - if (action.hpLoss) { - damage = action.hpLoss * difficultyMultiplier; - damage = Math.ceil(addOffset(damage, 10)); - const cost = calculateHospitalizationCost(damage); - if (person.takeDamage(damage)) { - ++this.numHosp; - this.moneyLost += cost; - } - } - teamLossMax = Math.floor(teamCount); - - if (this.logging.blackops) { - this.log( - `${person.whoAmI()}: ` + - action.name + - " failed! Lost " + - formatNumberNoSuffix(rankLoss, 1) + - " rank and took " + - formatNumberNoSuffix(damage, 0) + - " damage", - ); - } - } - - this.resetAction(); // Stop regardless of success or fail - - // Calculate team losses - if (teamCount >= 1) { - const losses = getRandomInt(1, teamLossMax); - this.teamSize -= losses; - if (this.teamSize < this.sleeveSize) { - const sup = Player.sleeves.filter((x) => isSleeveSupportWork(x.currentWork)); - for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { - const r = Math.floor(Math.random() * sup.length); - sup[r].takeDamage(sup[r].hp.max); - sup.splice(r, 1); - } - this.teamSize += this.sleeveSize; - } - this.teamLost += losses; - if (this.logging.blackops) { - this.log( - `${person.whoAmI()}: You lost ${formatNumberNoSuffix(losses, 0)} team members during ${action.name}`, - ); - } - } - } catch (e: unknown) { - exceptionAlert(String(e)); - } - break; - } - case ActionTypes.Training: { - this.stamina -= 0.5 * BladeburnerConstants.BaseStaminaLoss; - const strExpGain = 30 * person.mults.strength_exp, - defExpGain = 30 * person.mults.defense_exp, - dexExpGain = 30 * person.mults.dexterity_exp, - agiExpGain = 30 * person.mults.agility_exp, - staminaGain = 0.04 * this.skillMultipliers.stamina; - retValue.strExp = strExpGain; - retValue.defExp = defExpGain; - retValue.dexExp = dexExpGain; - retValue.agiExp = agiExpGain; - this.staminaBonus += staminaGain; - if (this.logging.general) { - this.log( - `${person.whoAmI()}: ` + - "Training completed. Gained: " + - formatExp(strExpGain) + - " str exp, " + - formatExp(defExpGain) + - " def exp, " + - formatExp(dexExpGain) + - " dex exp, " + - formatExp(agiExpGain) + - " agi exp, " + - formatBigNumber(staminaGain) + - " max stamina", - ); - } - break; - } - case ActionTypes.FieldAnalysis: - case ActionTypes["Field Analysis"]: { - // Does not use stamina. Effectiveness depends on hacking, int, and cha - let eff = - 0.04 * Math.pow(person.skills.hacking, 0.3) + - 0.04 * Math.pow(person.skills.intelligence, 0.9) + - 0.02 * Math.pow(person.skills.charisma, 0.3); - eff *= person.mults.bladeburner_analysis; - if (isNaN(eff) || eff < 0) { - throw new Error("Field Analysis Effectiveness calculated to be NaN or negative"); - } - const hackingExpGain = 20 * person.mults.hacking_exp; - const charismaExpGain = 20 * person.mults.charisma_exp; - const rankGain = 0.1 * currentNodeMults.BladeburnerRank; - retValue.hackExp = hackingExpGain; - retValue.chaExp = charismaExpGain; - retValue.intExp = BladeburnerConstants.BaseIntGain; - this.changeRank(person, rankGain); - this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate); - if (this.logging.general) { - this.log( - `${person.whoAmI()}: ` + - `Field analysis completed. Gained ${formatBigNumber(rankGain)} rank, ` + - `${formatExp(hackingExpGain)} hacking exp, and ` + - `${formatExp(charismaExpGain)} charisma exp`, - ); - } - break; - } - case ActionTypes.Recruitment: { - const successChance = this.getRecruitmentSuccessChance(person); - const recruitTime = this.getRecruitmentTime(person) * 1000; - if (Math.random() < successChance) { - const expGain = 2 * BladeburnerConstants.BaseStatGain * recruitTime; - retValue.chaExp = expGain; - ++this.teamSize; - if (this.logging.general) { - this.log( - `${person.whoAmI()}: ` + - "Successfully recruited a team member! Gained " + - formatExp(expGain) + - " charisma exp", - ); - } - } else { - const expGain = BladeburnerConstants.BaseStatGain * recruitTime; - retValue.chaExp = expGain; - if (this.logging.general) { - this.log( - `${person.whoAmI()}: ` + - "Failed to recruit a team member. Gained " + - formatExp(expGain) + - " charisma exp", - ); - } - } - break; - } - case ActionTypes.Diplomacy: { - const eff = this.getDiplomacyEffectiveness(person); - this.getCurrentCity().chaos *= eff; - if (this.getCurrentCity().chaos < 0) { - this.getCurrentCity().chaos = 0; - } - if (this.logging.general) { - this.log( - `${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${formatPercent( - 1 - eff, - )}`, - ); - } - break; - } - case ActionTypes["Hyperbolic Regeneration Chamber"]: { - person.regenerateHp(BladeburnerConstants.HrcHpGain); - - const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100); - this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain); - if (this.logging.general) { - this.log( - `${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${ - BladeburnerConstants.HrcHpGain - } HP and gained ${formatStamina(staminaGain)} stamina`, - ); - } - break; - } - case ActionTypes["Incite Violence"]: { - for (const contract of Object.keys(this.contracts)) { - const growthF = Growths[contract]; - if (!growthF) throw new Error("trying to generate count for action that doesn't exist? " + contract); - this.contracts[contract].count += (60 * 3 * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod; - } - for (const operation of Object.keys(this.operations)) { - const growthF = Growths[operation]; - if (!growthF) throw new Error("trying to generate count for action that doesn't exist? " + operation); - this.operations[operation].count += (60 * 3 * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod; - } - if (this.logging.general) { - this.log(`${person.whoAmI()}: Incited violence in the synthoid communities.`); - } - for (const cityName of Object.values(CityName)) { - const city = this.cities[cityName]; - city.chaos += 10; - city.chaos += city.chaos / (Math.log(city.chaos) / Math.log(10)); - } - break; - } - default: - console.error(`Bladeburner.completeAction() called for invalid action: ${actionIdent.type}`); - break; - } - return retValue; - } - - infiltrateSynthoidCommunities(): void { - const infilSleeves = Player.sleeves.filter((s) => isSleeveInfiltrateWork(s.currentWork)).length; - const amt = Math.pow(infilSleeves, -0.5) / 2; - for (const contract of Object.keys(this.contracts)) { - this.contracts[contract].count += amt; - } - for (const operation of Object.keys(this.operations)) { - this.operations[operation].count += amt; - } - if (this.logging.general) { - this.log(`Sleeve: Infiltrate the synthoid communities.`); - } - } - - changeRank(person: Person, change: number): void { - if (isNaN(change)) { - throw new Error("NaN passed into Bladeburner.changeRank()"); - } - this.rank += change; - if (this.rank < 0) { - this.rank = 0; - } - this.maxRank = Math.max(this.rank, this.maxRank); - - const bladeburnersFactionName = FactionName.Bladeburners; - const bladeburnerFac = Factions[bladeburnersFactionName]; - if (bladeburnerFac.isMember) { - const favorBonus = 1 + bladeburnerFac.favor / 100; - bladeburnerFac.playerReputation += - BladeburnerConstants.RankToFactionRepFactor * change * person.mults.faction_rep * favorBonus; - } - - // Gain skill points - const rankNeededForSp = (this.totalSkillPoints + 1) * BladeburnerConstants.RanksPerSkillPoint; - if (this.maxRank >= rankNeededForSp) { - // Calculate how many skill points to gain - const gainedSkillPoints = Math.floor( - (this.maxRank - rankNeededForSp) / BladeburnerConstants.RanksPerSkillPoint + 1, - ); - this.skillPoints += gainedSkillPoints; - this.totalSkillPoints += gainedSkillPoints; - } - } - - processAction(seconds: number): void { - if (this.action.type === ActionTypes.Idle) return; - if (this.actionTimeToComplete <= 0) { - throw new Error(`Invalid actionTimeToComplete value: ${this.actionTimeToComplete}, type; ${this.action.type}`); - } - if (!this.action) { - throw new Error("Bladeburner.action is not an ActionIdentifier Object"); - } - //Check to see if action is a contract, and then to verify a sleeve didn't finish it first - if (this.action.type === 2) { - const remainingActions = this.contracts[this.action.name].count; - if (remainingActions < 1) { - return this.resetAction(); - } - } - // If the previous action went past its completion time, add to the next action - // This is not added immediately in case the automation changes the action - this.actionTimeCurrent += seconds + this.actionTimeOverflow; - this.actionTimeOverflow = 0; - if (this.actionTimeCurrent >= this.actionTimeToComplete) { - this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete; - const action = this.getActionObject(this.action); - const retValue = this.completeAction(Player, this.action); - Player.gainMoney(retValue.money, "bladeburner"); - Player.gainStats(retValue); - // Operation Daedalus - if (action == null) { - throw new Error("Failed to get BlackOperation Object for: " + this.action.name); - } else if (this.action.type != ActionTypes.BlackOperation && this.action.type != ActionTypes.BlackOp) { - this.startAction(this.action); // Repeat action - } - } - } - - calculateStaminaGainPerSecond(): number { - const effAgility = Player.skills.agility * this.skillMultipliers.effAgi; - const maxStaminaBonus = this.maxStamina / BladeburnerConstants.MaxStaminaToGainFactor; - const gain = (BladeburnerConstants.StaminaGainPerSecond + maxStaminaBonus) * Math.pow(effAgility, 0.17); - return gain * (this.skillMultipliers.stamina * Player.mults.bladeburner_stamina_gain); - } - - calculateMaxStamina(): void { - const effAgility = Player.skills.agility * this.skillMultipliers.effAgi; - const maxStamina = - (Math.pow(effAgility, 0.8) + this.staminaBonus) * - this.skillMultipliers.stamina * - Player.mults.bladeburner_max_stamina; - if (this.maxStamina !== maxStamina) { - const oldMax = this.maxStamina; - this.maxStamina = maxStamina; - this.stamina = (this.maxStamina * this.stamina) / oldMax; - } - if (isNaN(maxStamina)) { - throw new Error("Max Stamina calculated to be NaN in Bladeburner.calculateMaxStamina()"); - } - } - - create(): void { - this.contracts.Tracking = new Contract({ - name: "Tracking", - baseDifficulty: 125, - difficultyFac: 1.02, - rewardFac: 1.041, - rankGain: 0.3, - hpLoss: 0.5, - count: getRandomInt(25, 150), - weights: { - hack: 0, - str: 0.05, - def: 0.05, - dex: 0.35, - agi: 0.35, - cha: 0.1, - int: 0.05, - }, - decays: { - hack: 0, - str: 0.91, - def: 0.91, - dex: 0.91, - agi: 0.91, - cha: 0.9, - int: 1, - }, - isStealth: true, - }); - this.contracts["Bounty Hunter"] = new Contract({ - name: "Bounty Hunter", - baseDifficulty: 250, - difficultyFac: 1.04, - rewardFac: 1.085, - rankGain: 0.9, - hpLoss: 1, - count: getRandomInt(5, 150), - weights: { - hack: 0, - str: 0.15, - def: 0.15, - dex: 0.25, - agi: 0.25, - cha: 0.1, - int: 0.1, - }, - decays: { - hack: 0, - str: 0.91, - def: 0.91, - dex: 0.91, - agi: 0.91, - cha: 0.8, - int: 0.9, - }, - isKill: true, - }); - this.contracts.Retirement = new Contract({ - name: "Retirement", - baseDifficulty: 200, - difficultyFac: 1.03, - rewardFac: 1.065, - rankGain: 0.6, - hpLoss: 1, - count: getRandomInt(5, 150), - weights: { - hack: 0, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0.1, - int: 0.1, - }, - decays: { - hack: 0, - str: 0.91, - def: 0.91, - dex: 0.91, - agi: 0.91, - cha: 0.8, - int: 0.9, - }, - isKill: true, - }); - - this.operations.Investigation = new Operation({ - name: "Investigation", - baseDifficulty: 400, - difficultyFac: 1.03, - rewardFac: 1.07, - reqdRank: 25, - rankGain: 2.2, - rankLoss: 0.2, - count: getRandomInt(1, 100), - weights: { - hack: 0.25, - str: 0.05, - def: 0.05, - dex: 0.2, - agi: 0.1, - cha: 0.25, - int: 0.1, - }, - decays: { - hack: 0.85, - str: 0.9, - def: 0.9, - dex: 0.9, - agi: 0.9, - cha: 0.7, - int: 0.9, - }, - isStealth: true, - }); - this.operations["Undercover Operation"] = new Operation({ - name: "Undercover Operation", - baseDifficulty: 500, - difficultyFac: 1.04, - rewardFac: 1.09, - reqdRank: 100, - rankGain: 4.4, - rankLoss: 0.4, - hpLoss: 2, - count: getRandomInt(1, 100), - weights: { - hack: 0.2, - str: 0.05, - def: 0.05, - dex: 0.2, - agi: 0.2, - cha: 0.2, - int: 0.1, - }, - decays: { - hack: 0.8, - str: 0.9, - def: 0.9, - dex: 0.9, - agi: 0.9, - cha: 0.7, - int: 0.9, - }, - isStealth: true, - }); - this.operations["Sting Operation"] = new Operation({ - name: "Sting Operation", - baseDifficulty: 650, - difficultyFac: 1.04, - rewardFac: 1.095, - reqdRank: 500, - rankGain: 5.5, - rankLoss: 0.5, - hpLoss: 2.5, - count: getRandomInt(1, 150), - weights: { - hack: 0.25, - str: 0.05, - def: 0.05, - dex: 0.25, - agi: 0.1, - cha: 0.2, - int: 0.1, - }, - decays: { - hack: 0.8, - str: 0.85, - def: 0.85, - dex: 0.85, - agi: 0.85, - cha: 0.7, - int: 0.9, - }, - isStealth: true, - }); - this.operations.Raid = new Operation({ - name: "Raid", - baseDifficulty: 800, - difficultyFac: 1.045, - rewardFac: 1.1, - reqdRank: 3000, - rankGain: 55, - rankLoss: 2.5, - hpLoss: 50, - count: getRandomInt(1, 150), - weights: { - hack: 0.1, - str: 0.2, - def: 0.2, - dex: 0.2, - agi: 0.2, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.7, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.9, - }, - isKill: true, - }); - this.operations["Stealth Retirement Operation"] = new Operation({ - name: "Stealth Retirement Operation", - baseDifficulty: 1000, - difficultyFac: 1.05, - rewardFac: 1.11, - reqdRank: 20e3, - rankGain: 22, - rankLoss: 2, - hpLoss: 10, - count: getRandomInt(1, 150), - weights: { - hack: 0.1, - str: 0.1, - def: 0.1, - dex: 0.3, - agi: 0.3, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.7, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.9, - }, - isStealth: true, - isKill: true, - }); - this.operations.Assassination = new Operation({ - name: "Assassination", - baseDifficulty: 1500, - difficultyFac: 1.06, - rewardFac: 1.14, - reqdRank: 50e3, - rankGain: 44, - rankLoss: 4, - hpLoss: 5, - count: getRandomInt(1, 150), - weights: { - hack: 0.1, - str: 0.1, - def: 0.1, - dex: 0.3, - agi: 0.3, - cha: 0, - int: 0.1, - }, - decays: { - hack: 0.6, - str: 0.8, - def: 0.8, - dex: 0.8, - agi: 0.8, - cha: 0, - int: 0.8, - }, - isStealth: true, - isKill: true, - }); - } - - process(): void { - // Edge race condition when the engine checks the processing counters and attempts to route before the router is initialized. - if (!Router.isInitialized) return; - //safety measure this needs to be removed in a bigger refactor - this.resetBlackOps(); - - // If the Player starts doing some other actions, set action to idle and alert - if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true) && Player.currentWork) { - if (this.action.type !== ActionTypes.Idle) { - let msg = "Your Bladeburner action was cancelled because you started doing something else."; - if (this.automateEnabled) { - msg += `\n\nYour automation was disabled as well. You will have to re-enable it through the Bladeburner console`; - this.automateEnabled = false; - } - if (!Settings.SuppressBladeburnerPopup) { - dialogBoxCreate(msg); - } - } - this.resetAction(); - } - - // If the Player has no Stamina, set action to idle - if (this.stamina <= 0) { - this.log("Your Bladeburner action was cancelled because your stamina hit 0"); - this.resetAction(); - } - - // A 'tick' for this mechanic is one second (= 5 game cycles) - if (this.storedCycles >= BladeburnerConstants.CyclesPerSecond) { - let seconds = Math.floor(this.storedCycles / BladeburnerConstants.CyclesPerSecond); - seconds = Math.min(seconds, 5); // Max of 5 'ticks' - this.storedCycles -= seconds * BladeburnerConstants.CyclesPerSecond; - - // Stamina - this.calculateMaxStamina(); - this.stamina += this.calculateStaminaGainPerSecond() * seconds; - this.stamina = Math.min(this.maxStamina, this.stamina); - - // Count increase for contracts/operations - for (const contract of Object.values(this.contracts)) { - const growthF = Growths[contract.name]; - if (growthF === undefined) throw new Error(`growth formula for action '${contract.name}' is undefined`); - contract.count += (seconds * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod; - } - for (const op of Object.values(this.operations)) { - const growthF = Growths[op.name]; - if (growthF === undefined) throw new Error(`growth formula for action '${op.name}' is undefined`); - if (growthF !== undefined) { - op.count += (seconds * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod; - } - } - - // Chaos goes down very slowly - for (const cityName of Object.values(CityName)) { - const city = this.cities[cityName]; - if (!city) throw new Error("Invalid city when processing passive chaos reduction in Bladeburner.process"); - city.chaos -= 0.0001 * seconds; - city.chaos = Math.max(0, city.chaos); - } - - // Random Events - this.randomEventCounter -= seconds; - if (this.randomEventCounter <= 0) { - this.randomEvent(); - // Add instead of setting because we might have gone over the required time for the event - this.randomEventCounter += getRandomInt(240, 600); - } - - this.processAction(seconds); - - // Automation - if (this.automateEnabled) { - // Note: Do NOT set this.action = this.automateActionHigh/Low since it creates a reference - if (this.stamina <= this.automateThreshLow) { - if (this.action.name !== this.automateActionLow.name || this.action.type !== this.automateActionLow.type) { - this.action = new ActionIdentifier({ - type: this.automateActionLow.type, - name: this.automateActionLow.name, - }); - this.startAction(this.action); - } - } else if (this.stamina >= this.automateThreshHigh) { - if (this.action.name !== this.automateActionHigh.name || this.action.type !== this.automateActionHigh.type) { - this.action = new ActionIdentifier({ - type: this.automateActionHigh.type, - name: this.automateActionHigh.name, - }); - this.startAction(this.action); - } - } - } - - // Handle "nextUpdate" resolver after this update - if (BladeburnerPromise.resolve) { - BladeburnerPromise.resolve(seconds * 1000); - BladeburnerPromise.resolve = null; - BladeburnerPromise.promise = null; - } - } - } - - getTypeAndNameFromActionId(actionId: ActionIdentifier): { - type: string; - name: string; - } { - const res = { type: "", name: "" }; - const types = Object.keys(ActionTypes); - for (let i = 0; i < types.length; ++i) { - if (actionId.type === ActionTypes[types[i]]) { - res.type = types[i]; - break; - } - } - const gen = [ - "Training", - "Recruitment", - "FieldAnalysis", - "Field Analysis", - "Diplomacy", - "Hyperbolic Regeneration Chamber", - "Incite Violence", - ]; - if (gen.includes(res.type)) { - res.type = "General"; - } - - if (res.type == null) { - res.type = "Idle"; - } - - res.name = actionId.name != null ? actionId.name : "Idle"; - return res; - } - - getContractNamesNetscriptFn(): string[] { - return Object.keys(this.contracts); - } - - getOperationNamesNetscriptFn(): string[] { - return Object.keys(this.operations); - } - - getBlackOpNamesNetscriptFn(): string[] { - return Object.keys(BlackOperations); - } - - getGeneralActionNamesNetscriptFn(): string[] { - return Object.keys(GeneralActions); - } - - getSkillNamesNetscriptFn(): string[] { - return Object.keys(Skills); - } - - startActionNetscriptFn(type: string, name: string, workerScript: WorkerScript): boolean { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - workerScript.log("bladeburner.startAction", () => errorLogText); - return false; - } - - // Special logic for Black Ops - if (actionId.type === ActionTypes.BlackOp) { - const canRunOp = this.canAttemptBlackOp(actionId); - if (!canRunOp.isAvailable) { - workerScript.log("bladeburner.startAction", () => canRunOp.error + ""); - return false; - } - } - - try { - this.startAction(actionId); - if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true)) Player.finishWork(true); - workerScript.log( - "bladeburner.startAction", - () => `Starting bladeburner action with type '${type}' and name '${name}'`, - ); - return true; - } catch (e: unknown) { - console.error(e); - this.resetAction(); - workerScript.log("bladeburner.startAction", () => errorLogText); - return false; - } - } - - getActionTimeNetscriptFn(person: Person, type: string, name: string): number | string { - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - return "bladeburner.getActionTime"; - } - - const actionObj = this.getActionObject(actionId); - if (actionObj == null) { - return "bladeburner.getActionTime"; - } - switch (actionId.type) { - case ActionTypes.Contract: - case ActionTypes.Operation: - case ActionTypes.BlackOp: - case ActionTypes.BlackOperation: - return actionObj.getActionTime(this, person) * 1000; - case ActionTypes.Training: - case ActionTypes["Field Analysis"]: - case ActionTypes.FieldAnalysis: - return 30000; - case ActionTypes.Recruitment: - return this.getRecruitmentTime(person) * 1000; - case ActionTypes.Diplomacy: - case ActionTypes["Hyperbolic Regeneration Chamber"]: - case ActionTypes["Incite Violence"]: - return 60000; - default: - return "bladeburner.getActionTime"; - } - } - - getActionEstimatedSuccessChanceNetscriptFn(person: Person, type: string, name: string): [number, number] | string { - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - return "bladeburner.getActionEstimatedSuccessChance"; - } - - const actionObj = this.getActionObject(actionId); - if (actionObj == null) { - return "bladeburner.getActionEstimatedSuccessChance"; - } - switch (actionId.type) { - case ActionTypes.Contract: - case ActionTypes.Operation: - case ActionTypes.BlackOp: - case ActionTypes.BlackOperation: - return actionObj.getEstSuccessChance(this, person); - case ActionTypes.Training: - case ActionTypes["Field Analysis"]: - case ActionTypes.FieldAnalysis: - case ActionTypes.Diplomacy: - case ActionTypes["Hyperbolic Regeneration Chamber"]: - case ActionTypes["Incite Violence"]: - return [1, 1]; - case ActionTypes.Recruitment: { - const recChance = this.getRecruitmentSuccessChance(person); - return [recChance, recChance]; - } - default: - return "bladeburner.getActionEstimatedSuccessChance"; - } - } - - getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText); - return -1; - } - - const actionObj = this.getActionObject(actionId); - if (actionObj == null) { - workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText); - return -1; - } - - switch (actionId.type) { - case ActionTypes.Contract: - case ActionTypes.Operation: - return Math.floor(actionObj.count); - case ActionTypes.BlackOp: - case ActionTypes.BlackOperation: - if (this.blackops[name] != null) { - return 0; - } else { - return 1; - } - case ActionTypes.Training: - case ActionTypes.Recruitment: - case ActionTypes["Field Analysis"]: - case ActionTypes.FieldAnalysis: - case ActionTypes.Diplomacy: - case ActionTypes["Hyperbolic Regeneration Chamber"]: - case ActionTypes["Incite Violence"]: - return Infinity; - default: - workerScript.log("bladeburner.getActionCountRemaining", () => errorLogText); - return -1; - } - } - - getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number { - if (skillName === "" || !Object.hasOwn(Skills, skillName)) { - workerScript.log("bladeburner.getSkillLevel", () => `Invalid skill: '${skillName}'`); - return -1; - } - - if (this.skills[skillName] == null) { - return 0; - } else { - return this.skills[skillName]; - } - } - - getSkillUpgradeCostNetscriptFn(skillName: string, count: number, workerScript: WorkerScript): number { - if (skillName === "" || !Object.hasOwn(Skills, skillName)) { - workerScript.log("bladeburner.getSkillUpgradeCost", () => `Invalid skill: '${skillName}'`); - return -1; - } - - const skill = Skills[skillName]; - const currentLevel = this.skills[skillName] ?? 0; - - if (skill.maxLvl !== 0 && currentLevel + count > skill.maxLvl) { - return Infinity; - } - - return skill.calculateCost(currentLevel, count); - } - - upgradeSkillNetscriptFn(skillName: string, count: number, workerScript: WorkerScript): boolean { - const errorLogText = `Invalid skill: '${skillName}'`; - if (!Object.hasOwn(Skills, skillName)) { - workerScript.log("bladeburner.upgradeSkill", () => errorLogText); - return false; - } - - const skill = Skills[skillName]; - let currentLevel = 0; - if (this.skills[skillName] && !isNaN(this.skills[skillName])) { - currentLevel = this.skills[skillName]; - } - const cost = skill.calculateCost(currentLevel, count); - - if (skill.maxLvl && currentLevel + count > skill.maxLvl) { - workerScript.log("bladeburner.upgradeSkill", () => `Skill '${skillName}' cannot be upgraded ${count} time(s).`); - return false; - } - - if (this.skillPoints < cost) { - workerScript.log( - "bladeburner.upgradeSkill", - () => - `You do not have enough skill points to upgrade ${skillName} ${count} time(s). (You have ${this.skillPoints}, you need ${cost})`, - ); - return false; - } - - this.skillPoints -= cost; - this.upgradeSkill(skill, count); - workerScript.log("bladeburner.upgradeSkill", () => `'${skillName}' upgraded to level ${this.skills[skillName]}`); - return true; - } - - getTeamSizeNetscriptFn(type: string, name: string, workerScript: WorkerScript): number { - if (type === "" && name === "") { - return this.teamSize; - } - - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - workerScript.log("bladeburner.getTeamSize", () => errorLogText); - return -1; - } - - const actionObj = this.getActionObject(actionId); - if (actionObj == null) { - workerScript.log("bladeburner.getTeamSize", () => errorLogText); - return -1; - } - - if ( - actionId.type === ActionTypes.Operation || - actionId.type === ActionTypes.BlackOp || - actionId.type === ActionTypes.BlackOperation - ) { - return actionObj.teamCount; - } else { - return 0; - } - } - - setTeamSizeNetscriptFn(type: string, name: string, size: number, workerScript: WorkerScript): number { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - const actionId = this.getActionIdFromTypeAndName(type, name); - if (actionId == null) { - workerScript.log("bladeburner.setTeamSize", () => errorLogText); - return -1; - } - - if ( - actionId.type !== ActionTypes.Operation && - actionId.type !== ActionTypes.BlackOp && - actionId.type !== ActionTypes.BlackOperation - ) { - workerScript.log("bladeburner.setTeamSize", () => "Only valid for 'Operations' and 'BlackOps'"); - return -1; - } - - const actionObj = this.getActionObject(actionId); - if (actionObj == null) { - workerScript.log("bladeburner.setTeamSize", () => errorLogText); - return -1; - } - - let sanitizedSize = Math.round(size); - if (isNaN(sanitizedSize) || sanitizedSize < 0) { - workerScript.log("bladeburner.setTeamSize", () => `Invalid size: ${size}`); - return -1; - } - if (this.teamSize < sanitizedSize) { - sanitizedSize = this.teamSize; - } - actionObj.teamCount = sanitizedSize; - workerScript.log("bladeburner.setTeamSize", () => `Team size for '${name}' set to ${sanitizedSize}.`); - return sanitizedSize; - } - - joinBladeburnerFactionNetscriptFn(workerScript: WorkerScript): boolean { - const bladeburnerFac = Factions[FactionName.Bladeburners]; - if (bladeburnerFac.isMember) { - return true; - } else if (this.rank >= BladeburnerConstants.RankNeededForFaction) { - joinFaction(bladeburnerFac); - workerScript.log("bladeburner.joinBladeburnerFaction", () => `Joined ${FactionName.Bladeburners} faction.`); - return true; - } else { - workerScript.log( - "bladeburner.joinBladeburnerFaction", - () => `You do not have the required rank (${this.rank}/${BladeburnerConstants.RankNeededForFaction}).`, - ); - return false; - } - } - - /** Serialize the current object to a JSON save state. */ - toJSON(): IReviverValue { - return Generic_toJSON("Bladeburner", this); - } - - /** Initializes a Bladeburner object from a JSON save state. */ - static fromJSON(value: IReviverValue): Bladeburner { - return Generic_fromJSON(Bladeburner, value.data); - } -} - -constructorsForReviver.Bladeburner = Bladeburner; diff --git a/src/Bladeburner/City.ts b/src/Bladeburner/City.ts index 9e6e265de..8d31045b8 100644 --- a/src/Bladeburner/City.ts +++ b/src/Bladeburner/City.ts @@ -1,35 +1,15 @@ +import { CityName } from "@enums"; import { BladeburnerConstants } from "./data/Constants"; import { getRandomInt } from "../utils/helpers/getRandomInt"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; import { addOffset } from "../utils/helpers/addOffset"; -import { CityName } from "@enums"; - -interface IChangePopulationByCountParams { - /** How much the estimate should change by. */ - estChange: number; - /** Add offset to estimate (offset by percentage). */ - estOffset: number; -} - -interface IChangePopulationByPercentageParams { - nonZero: boolean; - changeEstEqually: boolean; -} +import { clampInteger, clampNumber } from "../utils/helpers/clampNumber"; export class City { - /** Name of the city. */ name: CityName; - - /** Population of the city. */ - pop = 0; - - /** Population estimation of the city. */ - popEst = 0; - - /** Number of communities in the city. */ - comms = 0; - - /** Chaos level of the city. */ + pop = 0; // Population + popEst = 0; // Population estimate + comms = 0; // Number of communities chaos = 0; constructor(name = CityName.Sector12) { @@ -44,118 +24,63 @@ export class City { this.chaos = 0; } - /** p is the percentage, not the multiplier (e.g. pass in p = 5 for 5%) */ + /** @param {number} p - the percentage change, not the multiplier. e.g. pass in p = 5 for 5% */ changeChaosByPercentage(p: number): void { - if (isNaN(p)) { - throw new Error("NaN passed into City.chaosChaosByPercentage()"); - } - if (p === 0) { - return; - } - this.chaos += this.chaos * (p / 100); - if (this.chaos < 0) { - this.chaos = 0; - } + this.chaos = clampNumber(this.chaos * (1 + p / 100), 0); } improvePopulationEstimateByCount(n: number): void { - if (isNaN(n)) { - throw new Error("NaN passed into City.improvePopulationEstimateByCount()"); - } - if (this.popEst < this.pop) { - this.popEst += n; - if (this.popEst > this.pop) { - this.popEst = this.pop; - } - } else if (this.popEst > this.pop) { - this.popEst -= n; - if (this.popEst < this.pop) { - this.popEst = this.pop; - } - } + n = clampInteger(n, 0); + const diff = Math.abs(this.popEst - this.pop); + // Chgnge would overshoot actual population -> make estimate accurate + if (diff <= n) this.popEst = this.pop; + // Otherwise make enstimate closer by n + else if (this.popEst < this.pop) this.popEst += n; + else this.popEst -= n; } - /** p is the percentage, not the multiplier (e.g. pass in p = 5 for 5%) */ + /** @param {number} p - the percentage change, not the multiplier. e.g. pass in p = 5 for 5% */ improvePopulationEstimateByPercentage(p: number, skillMult = 1): void { - p = p * skillMult; - if (isNaN(p)) { - throw new Error("NaN passed into City.improvePopulationEstimateByPercentage()"); - } - if (this.popEst < this.pop) { - ++this.popEst; // In case estimate is 0 - this.popEst *= 1 + p / 100; - if (this.popEst > this.pop) { - this.popEst = this.pop; - } - } else if (this.popEst > this.pop) { - this.popEst *= 1 - p / 100; - if (this.popEst < this.pop) { - this.popEst = this.pop; - } - } + p = clampNumber((p * skillMult) / 100); + const diff = Math.abs(this.popEst - this.pop); + // Chgnge would overshoot actual population -> make estimate accurate + if (diff <= p * this.popEst) this.popEst = this.pop; + // Otherwise make enstimate closer by n + else if (this.popEst < this.pop) this.popEst = clampNumber(this.popEst * (1 + p)); + else this.popEst = clampNumber(this.popEst * (1 - p)); } - changePopulationByCount(n: number, params: IChangePopulationByCountParams = { estChange: 0, estOffset: 0 }): void { - if (isNaN(n)) { - throw new Error("NaN passed into City.changePopulationByCount()"); - } - this.pop += n; + /** + * @param params.estChange - Number to change the estimate by + * @param params.estOffset - Offset percentage to apply to estimate */ + changePopulationByCount(n: number, params = { estChange: 0, estOffset: 0 }): void { + n = clampInteger(n); + this.pop = clampInteger(this.pop + n, 0); if (params.estChange && !isNaN(params.estChange)) { this.popEst += params.estChange; } if (params.estOffset) { this.popEst = addOffset(this.popEst, params.estOffset); } - this.popEst = Math.max(this.popEst, 0); + this.popEst = clampInteger(this.popEst, 0); } /** - * @p is the percentage, not the multiplier. e.g. pass in p = 5 for 5% - * @params options: - * 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: 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)); + * @param {number} p - the percentage change, not the multiplier. e.g. pass in p = 5 for 5% + * @param {boolean} params.changeEstEqually - Whether to change the population estimate by an equal amount + * @param {boolean} params.nonZero - Whether to ensure that population always changes by at least 1 */ + changePopulationByPercentage(p: number, params = { nonZero: false, changeEstEqually: false }): number { + let change = clampInteger(this.pop * (p / 100)); - // Population always changes by at least 1 - if (params.nonZero && change === 0) { - p > 0 ? (change = 1) : (change = -1); - } + if (params.nonZero && change === 0) change = p > 0 ? 1 : -1; - this.pop += change; - if (params.changeEstEqually) { - this.popEst += change; - if (this.popEst < 0) { - this.popEst = 0; - } - } + this.pop = clampInteger(this.pop + change, 0); + if (params.changeEstEqually) this.popEst = clampInteger(this.popEst + change, 0); return change; } changeChaosByCount(n: number): void { - if (isNaN(n)) { - throw new Error("NaN passed into City.changeChaosByCount()"); - } - if (n === 0) { - return; - } - this.chaos += n; - if (this.chaos < 0) { - this.chaos = 0; - } + this.chaos = clampNumber(this.chaos + n, 0); } /** Serialize the current object to a JSON save state. */ diff --git a/src/Bladeburner/Contract.ts b/src/Bladeburner/Contract.ts deleted file mode 100644 index d9baa7b94..000000000 --- a/src/Bladeburner/Contract.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Bladeburner } from "./Bladeburner"; -import { Action, IActionParams } from "./Action"; -import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; - -export class Contract extends Action { - constructor(params: IActionParams | null = null) { - super(params); - } - - getActionTypeSkillSuccessBonus(inst: Bladeburner): number { - return inst.skillMultipliers.successChanceContract; - } - - toJSON(): IReviverValue { - return Generic_toJSON("Contract", this); - } - - static fromJSON(value: IReviverValue): Contract { - return Generic_fromJSON(Contract, value.data); - } -} - -constructorsForReviver.Contract = Contract; diff --git a/src/Bladeburner/Enums.ts b/src/Bladeburner/Enums.ts index 60ce5dee5..0b251c012 100644 --- a/src/Bladeburner/Enums.ts +++ b/src/Bladeburner/Enums.ts @@ -1,4 +1,31 @@ -export enum BlackOperationName { +export enum BladeActionType { + general = "General", + contract = "Contracts", + operation = "Operations", + blackOp = "Black Operations", +} +export enum BladeGeneralActionName { + training = "Training", + fieldAnalysis = "Field Analysis", + recruitment = "Recruitment", + diplomacy = "Diplomacy", + hyperbolicRegen = "Hyperbolic Regeneration Chamber", + inciteViolence = "Incite Violence", +} +export enum BladeContractName { + tracking = "Tracking", + bountyHunter = "Bounty Hunter", + retirement = "Retirement", +} +export enum BladeOperationName { + investigation = "Investigation", + undercover = "Undercover Operation", + sting = "Sting Operation", + raid = "Raid", + stealthRetirement = "Stealth Retirement Operation", + assassination = "Assassination", +} +export enum BladeBlackOpName { OperationTyphoon = "Operation Typhoon", OperationZero = "Operation Zero", OperationX = "Operation X", @@ -21,3 +48,36 @@ export enum BlackOperationName { OperationVindictus = "Operation Vindictus", OperationDaedalus = "Operation Daedalus", } + +export enum BladeSkillName { + bladesIntuition = "Blade's Intuition", + cloak = "Cloak", + shortCircuit = "Short-Circuit", + digitalObserver = "Digital Observer", + tracer = "Tracer", + overclock = "Overclock", + reaper = "Reaper", + evasiveSystem = "Evasive System", + datamancer = "Datamancer", + cybersEdge = "Cyber's Edge", + handsOfMidas = "Hands of Midas", + hyperdrive = "Hyperdrive", +} + +export enum BladeMultName { + successChanceAll = "Total Success Chance", + successChanceStealth = "Stealth Success Chance", + successChanceKill = "Retirement Success Chance", + successChanceContract = "Contract Success Chance", + successChanceOperation = "Operation Success Chance", + successChanceEstimate = "Synthoid Data Estimate", + actionTime = "Action Time", + effStr = "Effective Strength", + effDef = "Effective Defense", + effDex = "Effective Dexterity", + effAgi = "Effective Agility", + effCha = "Effective Charisma", + stamina = "Stamina", + money = "Contract Money", + expGain = "Experience Gain", +} diff --git a/src/Bladeburner/GeneralActions.tsx b/src/Bladeburner/GeneralActions.tsx deleted file mode 100644 index 496ee57c8..000000000 --- a/src/Bladeburner/GeneralActions.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Action } from "./Action"; - -export const GeneralActions: Record = {}; - -const actionNames: string[] = [ - "Training", - "Field Analysis", - "Recruitment", - "Diplomacy", - "Hyperbolic Regeneration Chamber", - "Incite Violence", -]; - -for (const actionName of actionNames) { - GeneralActions[actionName] = new Action({ - name: actionName, - }); -} diff --git a/src/Bladeburner/Operation.ts b/src/Bladeburner/Operation.ts deleted file mode 100644 index dfc257eb6..000000000 --- a/src/Bladeburner/Operation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Bladeburner } from "./Bladeburner"; -import { BladeburnerConstants } from "./data/Constants"; -import { Action, IActionParams } from "./Action"; -import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; - -export interface IOperationParams extends IActionParams { - reqdRank?: number; - teamCount?: number; -} - -export class Operation extends Action { - reqdRank = 100; - teamCount = 0; - - constructor(params: IOperationParams | null = null) { - super(params); - if (params && params.reqdRank) this.reqdRank = params.reqdRank; - if (params && params.teamCount) this.teamCount = params.teamCount; - } - - // For actions that have teams. To be implemented by subtypes. - getTeamSuccessBonus(inst: Bladeburner): number { - if (this.teamCount && this.teamCount > 0) { - this.teamCount = Math.min(this.teamCount, inst.teamSize); - const teamMultiplier = Math.pow(this.teamCount, 0.05); - return teamMultiplier; - } - - return 1; - } - - getActionTypeSkillSuccessBonus(inst: Bladeburner): number { - return inst.skillMultipliers.successChanceOperation; - } - - getChaosDifficultyBonus(inst: Bladeburner /*, params: ISuccessChanceParams*/): number { - const city = inst.getCurrentCity(); - if (city.chaos > BladeburnerConstants.ChaosThreshold) { - const diff = 1 + (city.chaos - BladeburnerConstants.ChaosThreshold); - const mult = Math.pow(diff, 0.5); - return mult; - } - - return 1; - } - - toJSON(): IReviverValue { - return Generic_toJSON("Operation", this); - } - - static fromJSON(value: IReviverValue): Operation { - return Generic_fromJSON(Operation, value.data); - } -} - -constructorsForReviver.Operation = Operation; diff --git a/src/Bladeburner/Skill.ts b/src/Bladeburner/Skill.ts index 3beb3c73a..099bb63b4 100644 --- a/src/Bladeburner/Skill.ts +++ b/src/Bladeburner/Skill.ts @@ -1,149 +1,51 @@ +import type { BladeMultName, BladeSkillName } from "@enums"; + import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; +import { Bladeburner } from "./Bladeburner"; +import { Availability } from "./Types"; +import { PositiveSafeInteger, isPositiveSafeInteger } from "../types"; +import { PartialRecord, getRecordEntries } from "../Types/Record"; -interface ISkillParams { - name: string; +interface SkillParams { + name: BladeSkillName; desc: string; - baseCost?: number; costInc?: number; maxLvl?: number; - - successChanceAll?: number; - successChanceStealth?: number; - successChanceKill?: number; - successChanceContract?: number; - successChanceOperation?: number; - successChanceEstimate?: number; - - actionTime?: number; - - effHack?: number; - effStr?: number; - effDef?: number; - effDex?: number; - effAgi?: number; - effCha?: number; - - stamina?: number; - money?: number; - expGain?: number; + mults: PartialRecord; } export class Skill { - name: string; + name: BladeSkillName; desc: string; // Cost is in Skill Points baseCost = 1; // Additive cost increase per level costInc = 1; - maxLvl = 0; + maxLvl = Number.MAX_SAFE_INTEGER; + mults: PartialRecord = {}; - /** - * These benefits are additive. So total multiplier will be level (handled externally) times the - * effects below - */ - successChanceAll = 0; - successChanceStealth = 0; - successChanceKill = 0; - successChanceContract = 0; - successChanceOperation = 0; - - /** - * This multiplier affects everything that increases synthoid population/community estimate - * e.g. Field analysis, Investigation Op, Undercover Op - */ - successChanceEstimate = 0; - actionTime = 0; - effHack = 0; - effStr = 0; - effDef = 0; - effDex = 0; - effAgi = 0; - effCha = 0; - stamina = 0; - money = 0; - expGain = 0; - - constructor(params: ISkillParams = { name: "foo", desc: "foo" }) { - if (!params.name) { - throw new Error("Failed to initialize Bladeburner Skill. No name was specified in ctor"); - } - if (!params.desc) { - throw new Error("Failed to initialize Bladeburner Skills. No desc was specified in ctor"); - } + constructor(params: SkillParams) { this.name = params.name; this.desc = params.desc; - this.baseCost = params.baseCost ? params.baseCost : 1; - this.costInc = params.costInc ? params.costInc : 1; - - if (params.maxLvl) { - this.maxLvl = params.maxLvl; - } - - if (params.successChanceAll) { - this.successChanceAll = params.successChanceAll; - } - if (params.successChanceStealth) { - this.successChanceStealth = params.successChanceStealth; - } - if (params.successChanceKill) { - this.successChanceKill = params.successChanceKill; - } - if (params.successChanceContract) { - this.successChanceContract = params.successChanceContract; - } - if (params.successChanceOperation) { - this.successChanceOperation = params.successChanceOperation; - } - - if (params.successChanceEstimate) { - this.successChanceEstimate = params.successChanceEstimate; - } - - if (params.actionTime) { - this.actionTime = params.actionTime; - } - if (params.effHack) { - this.effHack = params.effHack; - } - if (params.effStr) { - this.effStr = params.effStr; - } - if (params.effDef) { - this.effDef = params.effDef; - } - if (params.effDex) { - this.effDex = params.effDex; - } - if (params.effAgi) { - this.effAgi = params.effAgi; - } - if (params.effCha) { - this.effCha = params.effCha; - } - - if (params.stamina) { - this.stamina = params.stamina; - } - if (params.money) { - this.money = params.money; - } - if (params.expGain) { - this.expGain = params.expGain; - } + this.baseCost = params.baseCost ?? 1; + this.costInc = params.costInc ?? 1; + this.maxLvl = params.maxLvl ?? 1; + for (const [multName, mult] of getRecordEntries(params.mults)) this.mults[multName] = mult; } - calculateCost(currentLevel: number, count = 1): number { + calculateCost(currentLevel: number, count = 1 as PositiveSafeInteger): number { + if (currentLevel + count > this.maxLvl) return Infinity; //Recursive mode does not handle invalid inputs properly, but it should never //be possible for it to run with them. For the sake of not crashing the game, - const recursiveMode = (currentLevel: number, count: number): number => { + const recursiveMode = (currentLevel: number, count: PositiveSafeInteger): number => { if (count <= 1) { return Math.floor((this.baseCost + currentLevel * this.costInc) * currentNodeMults.BladeburnerSkillCost); } else { const thisUpgrade = Math.floor( (this.baseCost + currentLevel * this.costInc) * currentNodeMults.BladeburnerSkillCost, ); - return this.calculateCost(currentLevel + 1, count - 1) + thisUpgrade; + return this.calculateCost(currentLevel + 1, (count - 1) as PositiveSafeInteger) + thisUpgrade; } }; @@ -166,26 +68,16 @@ export class Skill { } } - getMultiplier(name: string): number { - 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; + canUpgrade(bladeburner: Bladeburner, count = 1): Availability<{ cost: number }> { + const currentLevel = bladeburner.skills[this.name] ?? 0; + if (!isPositiveSafeInteger(count)) return { error: `Invalid upgrade count ${count}` }; + if (currentLevel + count > this.maxLvl) return { error: `Upgraded level ${currentLevel + count} exceeds max` }; + const cost = this.calculateCost(currentLevel, count); + if (cost > bladeburner.skillPoints) return { error: `Insufficient skill points for upgrade` }; + return { available: true, cost }; + } - 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; + getMultiplier(name: BladeMultName): number { + return this.mults[name] ?? 0; } } diff --git a/src/Bladeburner/Types.ts b/src/Bladeburner/Types.ts new file mode 100644 index 000000000..0782e30bc --- /dev/null +++ b/src/Bladeburner/Types.ts @@ -0,0 +1,31 @@ +import type { BlackOperation, Contract, GeneralAction, Operation } from "./Actions"; +import type { + BladeActionType, + BladeBlackOpName, + BladeContractName, + BladeOperationName, + BladeGeneralActionName, +} from "@enums"; + +export interface SuccessChanceParams { + /** Whether the success chance should be based on estimated statistics */ + est: boolean; +} + +type AvailabilitySuccess = { available: true } & T; +type AvailabilityFailure = { available?: undefined; error: string }; +export type Availability = AvailabilitySuccess | AvailabilityFailure; + +type AttemptSuccess = { success: true; message?: string } & T; +type AttemptFailure = { success?: undefined; message: string }; +export type Attempt = AttemptSuccess | AttemptFailure; + +export type Action = Contract | Operation | BlackOperation | GeneralAction; + +export type ActionIdentifier = + | { type: BladeActionType.blackOp; name: BladeBlackOpName } + | { type: BladeActionType.contract; name: BladeContractName } + | { type: BladeActionType.operation; name: BladeOperationName } + | { type: BladeActionType.general; name: BladeGeneralActionName }; + +export type LevelableAction = Contract | Operation; diff --git a/src/Bladeburner/data/ActionTypes.ts b/src/Bladeburner/data/ActionTypes.ts deleted file mode 100644 index 674e463ab..000000000 --- a/src/Bladeburner/data/ActionTypes.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Action Identifier enum -export const ActionTypes: { - [key: string]: number; - Idle: number; - Contract: number; - Operation: number; - BlackOp: number; - BlackOperation: number; - Training: number; - Recruitment: number; - FieldAnalysis: number; - "Field Analysis": number; - Diplomacy: number; - "Hyperbolic Regeneration Chamber": number; - "Incite Violence": number; -} = { - Idle: 1, - Contract: 2, - Operation: 3, - BlackOp: 4, - BlackOperation: 4, - Training: 5, - Recruitment: 6, - FieldAnalysis: 7, - "Field Analysis": 7, - Diplomacy: 8, - "Hyperbolic Regeneration Chamber": 9, - "Incite Violence": 10, -}; diff --git a/src/Bladeburner/data/BlackOperations.ts b/src/Bladeburner/data/BlackOperations.ts new file mode 100644 index 000000000..b5aba839d --- /dev/null +++ b/src/Bladeburner/data/BlackOperations.ts @@ -0,0 +1,737 @@ +import { BlackOperation } from "../Actions/BlackOperation"; +import { BladeBlackOpName, CityName, FactionName } from "@enums"; + +export const BlackOperations: Record = { + [BladeBlackOpName.OperationTyphoon]: new BlackOperation({ + name: BladeBlackOpName.OperationTyphoon, + n: 0, + baseDifficulty: 2000, + reqdRank: 2.5e3, + rankGain: 50, + rankLoss: 10, + hpLoss: 100, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "Obadiah Zenyatta is the leader of a RedWater PMC. It has long been known among the intelligence community " + + "that Zenyatta, along with the rest of the PMC, is a Synthoid.\n\n" + + `The goal of ${BladeBlackOpName.OperationTyphoon} is to find and eliminate Zenyatta and RedWater by any means ` + + "necessary. After the task is completed, the actions must be covered up from the general public.", + }), + [BladeBlackOpName.OperationZero]: new BlackOperation({ + name: BladeBlackOpName.OperationZero, + n: 1, + baseDifficulty: 2500, + reqdRank: 5e3, + rankGain: 60, + rankLoss: 15, + hpLoss: 50, + weights: { + hacking: 0.2, + strength: 0.15, + defense: 0.15, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isStealth: true, + desc: + "AeroCorp is one of the world's largest defense contractors. Its leader, Steve Watataki, is thought to be " + + "a supporter of Synthoid rights. He must be removed.\n\n" + + `The goal of ${BladeBlackOpName.OperationZero} is to covertly infiltrate AeroCorp and uncover any incriminating ` + + "evidence or information against Watataki that will cause him to be removed from his position at AeroCorp. " + + "Incriminating evidence can be fabricated as a last resort. Be warned that AeroCorp has some of the most advanced " + + "security measures in the world.", + }), + [BladeBlackOpName.OperationX]: new BlackOperation({ + name: BladeBlackOpName.OperationX, + n: 2, + baseDifficulty: 3000, + reqdRank: 7.5e3, + rankGain: 75, + rankLoss: 15, + hpLoss: 100, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "We have recently discovered an underground publication group called Samizdat. Even though most of their " + + "publications are nonsensical conspiracy theories, the average human is gullible enough to believe them. Many of " + + "their works discuss Synthoids and pose a threat to society. The publications are spreading rapidly in China and " + + "other Eastern countries.\n\n" + + "Samizdat has done a good job of keeping hidden and anonymous. However, we've just received intelligence that " + + `their base of operations is in ${CityName.Ishima}'s underground sewer systems. Your task is to investigate the ` + + "sewer systems, and eliminate Samizdat. They must never publish anything again.", + }), + [BladeBlackOpName.OperationTitan]: new BlackOperation({ + name: BladeBlackOpName.OperationTitan, + n: 3, + baseDifficulty: 4000, + reqdRank: 10e3, + rankGain: 100, + rankLoss: 20, + hpLoss: 100, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "Several months ago Titan Laboratories' Bioengineering department was infiltrated by Synthoids. As far as we " + + "know, Titan Laboratories' management has no knowledge about this. We don't know what the Synthoids are up to, " + + "but the research that they could be conducting using Titan Laboratories' vast resources is potentially very " + + "dangerous.\n\n" + + `Your goal is to enter and destroy the Bioengineering department's facility in ${CityName.Aevum}. The task is not ` + + "just to retire the Synthoids there, but also to destroy any information or research at the facility that is " + + "relevant to the Synthoids and their goals.", + }), + [BladeBlackOpName.OperationAres]: new BlackOperation({ + name: BladeBlackOpName.OperationAres, + n: 4, + baseDifficulty: 5000, + reqdRank: 12.5e3, + rankGain: 125, + rankLoss: 20, + hpLoss: 200, + weights: { + hacking: 0, + strength: 0.25, + defense: 0.25, + dexterity: 0.25, + agility: 0.25, + charisma: 0, + intelligence: 0, + }, + decays: { + hacking: 0, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "One of our undercover agents, Agent Carter, has informed us of a massive weapons deal going down in Dubai " + + "between rogue Russian militants and a radical Synthoid community. These weapons are next-gen plasma and energy " + + "weapons. It is critical for the safety of humanity that this deal does not happen.\n\n" + + "Your task is to intercept the deal. Leave no survivors.", + }), + [BladeBlackOpName.OperationArchangel]: new BlackOperation({ + name: BladeBlackOpName.OperationArchangel, + n: 5, + baseDifficulty: 7500, + reqdRank: 15e3, + rankGain: 200, + rankLoss: 20, + hpLoss: 25, + weights: { + hacking: 0, + strength: 0.2, + defense: 0.2, + dexterity: 0.3, + agility: 0.3, + charisma: 0, + intelligence: 0, + }, + decays: { + hacking: 0, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "Our analysts have discovered that the popular Red Rabbit brothel in Amsterdam is run and 'staffed' by MK-VI " + + "Synthoids. Intelligence suggests that the profit from this brothel is used to fund a large black market arms " + + "trafficking operation.\n\n" + + "The goal of this operation is to take out the leaders that are running the Red Rabbit brothel. Try to limit the " + + "number of other casualties, but do what you must to complete the mission.", + }), + [BladeBlackOpName.OperationJuggernaut]: new BlackOperation({ + name: BladeBlackOpName.OperationJuggernaut, + n: 6, + baseDifficulty: 10e3, + reqdRank: 20e3, + rankGain: 300, + rankLoss: 40, + hpLoss: 300, + weights: { + hacking: 0, + strength: 0.25, + defense: 0.25, + dexterity: 0.25, + agility: 0.25, + charisma: 0, + intelligence: 0, + }, + decays: { + hacking: 0, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "The CIA has just encountered a new security threat. A new criminal group, lead by a shadowy operative who calls " + + "himself Juggernaut, has been smuggling drugs and weapons (including suspected bioweapons) into " + + `${CityName.Sector12}. We also have reason to believe they tried to break into one of Universal Energy's ` + + "facilities in order to cause a city-wide blackout. The CIA suspects that Juggernaut is a heavily-augmented " + + "Synthoid, and have thus enlisted our help.\n\n" + + "Your mission is to eradicate Juggernaut and his followers.", + }), + [BladeBlackOpName.OperationRedDragon]: new BlackOperation({ + name: BladeBlackOpName.OperationRedDragon, + n: 7, + baseDifficulty: 12.5e3, + reqdRank: 25e3, + rankGain: 500, + rankLoss: 50, + hpLoss: 500, + weights: { + hacking: 0.05, + strength: 0.2, + defense: 0.2, + dexterity: 0.25, + agility: 0.25, + charisma: 0, + intelligence: 0.05, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + `The ${FactionName.Tetrads} criminal organization is suspected of reverse-engineering the MK-VI Synthoid design. ` + + "We believe they altered and possibly improved the design and began manufacturing their own Synthoid models in" + + "order to bolster their criminal activities.\n\n" + + `Your task is to infiltrate and destroy the ${FactionName.Tetrads}' base of operations in Los Angeles. ` + + "Intelligence tells us that their base houses one of their Synthoid manufacturing units.", + }), + [BladeBlackOpName.OperationK]: new BlackOperation({ + name: BladeBlackOpName.OperationK, + n: 8, + baseDifficulty: 15e3, + reqdRank: 30e3, + rankGain: 750, + rankLoss: 60, + hpLoss: 1000, + weights: { + hacking: 0.05, + strength: 0.2, + defense: 0.2, + dexterity: 0.25, + agility: 0.25, + charisma: 0, + intelligence: 0.05, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "CODE RED SITUATION. Our intelligence tells us that VitaLife has discovered a new android cloning technology. " + + "This technology is supposedly capable of cloning Synthoids, not only physically but also their advanced AI " + + "modules. We do not believe that VitaLife is trying to use this technology illegally or maliciously, but if any " + + "Synthoids were able to infiltrate the corporation and take advantage of this technology then the results would " + + "be catastrophic.\n\n" + + "We do not have the power or jurisdiction to shut this down through legal or political means, so we must resort " + + "to a covert operation. Your goal is to destroy this technology and eliminate anyone who was involved in its " + + "creation.", + }), + [BladeBlackOpName.OperationDeckard]: new BlackOperation({ + name: BladeBlackOpName.OperationDeckard, + n: 9, + baseDifficulty: 20e3, + reqdRank: 40e3, + rankGain: 1e3, + rankLoss: 75, + hpLoss: 200, + weights: { + hacking: 0, + strength: 0.24, + defense: 0.24, + dexterity: 0.24, + agility: 0.24, + charisma: 0, + intelligence: 0.04, + }, + decays: { + hacking: 0, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "Despite your success in eliminating VitaLife's new android-replicating technology in " + + `${BladeBlackOpName.OperationK}, we've discovered that a small group of MK-VI Synthoids were able to make off with ` + + "the schematics and design of the technology before the Operation. It is almost a certainty that these Synthoids " + + "are some of the rogue MK-VI ones from the Synthoid Uprising.\n\n" + + `The goal of ${BladeBlackOpName.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need to ` + + "tell you how critical this mission is.", + }), + [BladeBlackOpName.OperationTyrell]: new BlackOperation({ + name: BladeBlackOpName.OperationTyrell, + n: 10, + baseDifficulty: 25e3, + reqdRank: 50e3, + rankGain: 1.5e3, + rankLoss: 100, + hpLoss: 500, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + `A week ago ${FactionName.BladeIndustries} reported a small break-in at one of their ${CityName.Aevum} ` + + `Augmentation storage facilities. We figured out that ${FactionName.TheDarkArmy} was behind the heist, and didn't think ` + + "any more of it. However, we've just discovered that several known MK-VI Synthoids were part of that break-in group.\n\n" + + "We cannot have Synthoids upgrading their already-enhanced abilities with Augmentations. Your task is to hunt " + + `down associated ${FactionName.TheDarkArmy} members and eliminate them.`, + }), + [BladeBlackOpName.OperationWallace]: new BlackOperation({ + name: BladeBlackOpName.OperationWallace, + n: 11, + baseDifficulty: 30e3, + reqdRank: 75e3, + rankGain: 2e3, + rankLoss: 150, + hpLoss: 1500, + weights: { + hacking: 0, + strength: 0.24, + defense: 0.24, + dexterity: 0.24, + agility: 0.24, + charisma: 0, + intelligence: 0.04, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + `Based on information gathered from ${BladeBlackOpName.OperationTyrell}, we've discovered that ` + + `${FactionName.TheDarkArmy} was well aware that there were Synthoids amongst their ranks. Even worse, we believe ` + + `that ${FactionName.TheDarkArmy} is working together with other criminal organizations such as ` + + `${FactionName.TheSyndicate} and that they are planning some sort of large-scale takeover of multiple major ` + + `cities, most notably ${CityName.Aevum}. We suspect that Synthoids have infiltrated the ranks of these criminal ` + + "factions and are trying to stage another Synthoid uprising.\n\n" + + "The best way to deal with this is to prevent it before it even happens. The goal of " + + `${BladeBlackOpName.OperationWallace} is to destroy ${FactionName.TheDarkArmy} and Syndicate factions in ` + + `${CityName.Aevum} immediately. Leave no survivors.`, + }), + [BladeBlackOpName.OperationShoulderOfOrion]: new BlackOperation({ + name: BladeBlackOpName.OperationShoulderOfOrion, + n: 12, + baseDifficulty: 35e3, + reqdRank: 100e3, + rankGain: 2.5e3, + rankLoss: 500, + hpLoss: 1500, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isStealth: true, + desc: + "China's Solaris Space Systems is secretly launching the first manned spacecraft in over a decade using " + + "Synthoids. We believe China is trying to establish the first off-world colonies.\n\n" + + "The mission is to prevent this launch without instigating an international conflict. When you accept this " + + "mission you will be officially disavowed by the NSA and the national government until after you successfully " + + "return. In the event of failure, all of the operation's team members must not let themselves be captured alive.", + }), + [BladeBlackOpName.OperationHyron]: new BlackOperation({ + name: BladeBlackOpName.OperationHyron, + n: 13, + baseDifficulty: 40e3, + reqdRank: 125e3, + rankGain: 3e3, + rankLoss: 1e3, + hpLoss: 500, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + `Our intelligence tells us that ${FactionName.FulcrumSecretTechnologies} is developing a quantum supercomputer ` + + "using human brains as core processors. This supercomputer is rumored to be able to store vast amounts of data " + + "and perform computations unmatched by any other supercomputer on the planet. But more importantly, the use of " + + "organic human brains means that the supercomputer may be able to reason abstractly and become self-aware.\n\n" + + "I do not need to remind you why sentient-level AIs pose a serious threat to all of mankind.\n\n" + + `The research for this project is being conducted at one of ${FactionName.FulcrumSecretTechnologies} secret ` + + `facilities in ${CityName.Aevum}, codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work, ` + + "and then find and kill the project lead.", + }), + [BladeBlackOpName.OperationMorpheus]: new BlackOperation({ + name: BladeBlackOpName.OperationMorpheus, + n: 14, + baseDifficulty: 45e3, + reqdRank: 150e3, + rankGain: 4e3, + rankLoss: 1e3, + hpLoss: 100, + weights: { + hacking: 0.05, + strength: 0.15, + defense: 0.15, + dexterity: 0.3, + agility: 0.3, + charisma: 0, + intelligence: 0.05, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isStealth: true, + desc: + "DreamSense Technologies is an advertising company that uses special technology to transmit their ads into the " + + "people's dreams and subconscious. They do this using broadcast transmitter towers. Based on information from our " + + `agents and informants in ${CityName.Chongqing}, we have reason to believe that one of the broadcast towers there ` + + "has been compromised by Synthoids and is being used to spread pro-Synthoid propaganda.\n\n" + + "The mission is to destroy this broadcast tower. Speed and stealth are of the utmost importance for this.", + }), + [BladeBlackOpName.OperationIonStorm]: new BlackOperation({ + name: BladeBlackOpName.OperationIonStorm, + n: 15, + baseDifficulty: 50e3, + reqdRank: 175e3, + rankGain: 5e3, + rankLoss: 1e3, + hpLoss: 5000, + weights: { + hacking: 0, + strength: 0.24, + defense: 0.24, + dexterity: 0.24, + agility: 0.24, + charisma: 0, + intelligence: 0.04, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the " + + `${CityName.Sector12} Slums. We don't know if they are rogue Synthoids from the Uprising, but we do know that they ` + + "have been stockpiling weapons, money, and other resources. This makes them dangerous.\n\n" + + `This is a full-scale assault operation to find and retire all of these Synthoids in the ${CityName.Sector12} ` + + "Slums.", + }), + [BladeBlackOpName.OperationAnnihilus]: new BlackOperation({ + name: BladeBlackOpName.OperationAnnihilus, + n: 16, + baseDifficulty: 55e3, + reqdRank: 200e3, + rankGain: 7.5e3, + rankLoss: 1e3, + hpLoss: 10e3, + weights: { + hacking: 0, + strength: 0.24, + defense: 0.24, + dexterity: 0.24, + agility: 0.24, + charisma: 0, + intelligence: 0.04, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + "Our superiors have ordered us to eradicate everything and everyone in an underground facility located in " + + `${CityName.Aevum}. They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist ` + + `organization called '${FactionName.TheCovenant}'. We have no prior intelligence about this organization, so you ` + + "are going in blind.", + }), + [BladeBlackOpName.OperationUltron]: new BlackOperation({ + name: BladeBlackOpName.OperationUltron, + n: 17, + baseDifficulty: 60e3, + reqdRank: 250e3, + rankGain: 10e3, + rankLoss: 2e3, + hpLoss: 10e3, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + isKill: true, + desc: + `${FactionName.OmniTekIncorporated}, the original designer and manufacturer of Synthoids, has notified us of a ` + + "malfunction in their AI design. This malfunction, when triggered, causes MK-VI Synthoids to become radicalized " + + "and seek out the destruction of humanity. They say that this bug affects all MK-VI Synthoids, not just the rogue " + + "ones from the Uprising.\n\n" + + `${FactionName.OmniTekIncorporated} has also told us they believe someone has triggered this malfunction in a ` + + "large group of MK-VI Synthoids, and that these newly-radicalized Synthoids are now amassing in " + + `${CityName.Volhaven} to form a terrorist group called Ultron.\n\n` + + "Intelligence suggests Ultron is heavily armed and that their members are augmented. We believe Ultron is making " + + "moves to take control of and weaponize DeltaOne's Tactical High-Energy Satellite Laser Array (THESLA).\n\n" + + "Your task is to find and destroy Ultron.", + }), + [BladeBlackOpName.OperationCenturion]: new BlackOperation({ + name: BladeBlackOpName.OperationCenturion, + n: 18, + baseDifficulty: 70e3, + reqdRank: 300e3, + rankGain: 15e3, + rankLoss: 5e3, + hpLoss: 10e3, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + desc: + "D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)\n\n" + + "Throughout all of humanity's history, we have relied on technology to survive, conquer, and progress. Its " + + "advancement became our primary goal. And at the peak of human civilization technology turned into power. Global, " + + "absolute power.\n\n" + + "It seems that the universe is not without a sense of irony.\n\n" + + "D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)", + }), + [BladeBlackOpName.OperationVindictus]: new BlackOperation({ + name: BladeBlackOpName.OperationVindictus, + n: 19, + baseDifficulty: 75e3, + reqdRank: 350e3, + rankGain: 20e3, + rankLoss: 20e3, + hpLoss: 20e3, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + desc: + "D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)\n\n" + + "The bits are all around us. The daemons that hold the Node together can manifest themselves in many different " + + "ways.\n\n" + + "D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)", + }), + [BladeBlackOpName.OperationDaedalus]: new BlackOperation({ + name: BladeBlackOpName.OperationDaedalus, + n: 20, + baseDifficulty: 80e3, + reqdRank: 400e3, + rankGain: 40e3, + rankLoss: 10e3, + hpLoss: 100e3, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.75, + }, + desc: "Yesterday we obeyed kings and bent our neck to emperors. Today we kneel only to truth.", + }), +}; + +/** Array for quick lookup by blackop number */ +export const blackOpsArray = Object.values(BlackOperations).sort((a, b) => (a.n < b.n ? -1 : 1)); +// Verify that all "n" properties match the index in the array +if (!blackOpsArray.every((blackOp, i) => blackOp.n === i)) { + throw new Error("blackOpsArray did not initialize with correct indices"); +} diff --git a/src/Bladeburner/data/BlackOperations.tsx b/src/Bladeburner/data/BlackOperations.tsx deleted file mode 100644 index 0dac80ae5..000000000 --- a/src/Bladeburner/data/BlackOperations.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React from "react"; -import { BlackOperationName, CityName, FactionName } from "@enums"; - -interface IBlackOp { - desc: JSX.Element; -} - -export const BlackOperations: Record = { - [BlackOperationName.OperationTyphoon]: { - desc: ( - <> - Obadiah Zenyatta is the leader of a RedWater PMC. It has long been known among the intelligence community that - Zenyatta, along with the rest of the PMC, is a Synthoid. -
-
- The goal of {BlackOperationName.OperationTyphoon} is to find and eliminate Zenyatta and RedWater by any means - necessary. After the task is completed, the actions must be covered up from the general public. - - ), - }, - - [BlackOperationName.OperationZero]: { - desc: ( - <> - AeroCorp is one of the world's largest defense contractors. Its leader, Steve Watataki, is thought to be a - supporter of Synthoid rights. He must be removed. -
-
- The goal of {BlackOperationName.OperationZero} is to covertly infiltrate AeroCorp and uncover any incriminating - evidence or information against Watataki that will cause him to be removed from his position at AeroCorp. - Incriminating evidence can be fabricated as a last resort. Be warned that AeroCorp has some of the most advanced - security measures in the world. - - ), - }, - [BlackOperationName.OperationX]: { - desc: ( - <> - We have recently discovered an underground publication group called Samizdat. Even though most of their - publications are nonsensical conspiracy theories, the average human is gullible enough to believe them. Many of - their works discuss Synthoids and pose a threat to society. The publications are spreading rapidly in China and - other Eastern countries. -
-
- Samizdat has done a good job of keeping hidden and anonymous. However, we've just received intelligence that - their base of operations is in {CityName.Ishima}'s underground sewer systems. Your task is to investigate the - sewer systems, and eliminate Samizdat. They must never publish anything again. - - ), - }, - [BlackOperationName.OperationTitan]: { - desc: ( - <> - Several months ago Titan Laboratories' Bioengineering department was infiltrated by Synthoids. As far as we - know, Titan Laboratories' management has no knowledge about this. We don't know what the Synthoids are up to, - but the research that they could be conducting using Titan Laboratories' vast resources is potentially very - dangerous. -
-
- Your goal is to enter and destroy the Bioengineering department's facility in {CityName.Aevum}. The task is not - just to retire the Synthoids there, but also to destroy any information or research at the facility that is - relevant to the Synthoids and their goals. - - ), - }, - [BlackOperationName.OperationAres]: { - desc: ( - <> - One of our undercover agents, Agent Carter, has informed us of a massive weapons deal going down in Dubai - between rogue Russian militants and a radical Synthoid community. These weapons are next-gen plasma and energy - weapons. It is critical for the safety of humanity that this deal does not happen. -
-
- Your task is to intercept the deal. Leave no survivors. - - ), - }, - [BlackOperationName.OperationArchangel]: { - desc: ( - <> - Our analysts have discovered that the popular Red Rabbit brothel in Amsterdam is run and 'staffed' by MK-VI - Synthoids. Intelligence suggests that the profit from this brothel is used to fund a large black market arms - trafficking operation. -
-
- The goal of this operation is to take out the leaders that are running the Red Rabbit brothel. Try to limit the - number of other casualties, but do what you must to complete the mission. - - ), - }, - [BlackOperationName.OperationJuggernaut]: { - desc: ( - <> - The CIA has just encountered a new security threat. A new criminal group, lead by a shadowy operative who calls - himself Juggernaut, has been smuggling drugs and weapons (including suspected bioweapons) into{" "} - {CityName.Sector12}. We also have reason to believe they tried to break into one of Universal Energy's - facilities in order to cause a city-wide blackout. The CIA suspects that Juggernaut is a heavily-augmented - Synthoid, and have thus enlisted our help. -
-
- Your mission is to eradicate Juggernaut and his followers. - - ), - }, - [BlackOperationName.OperationRedDragon]: { - desc: ( - <> - The {FactionName.Tetrads} criminal organization is suspected of reverse-engineering the MK-VI Synthoid design. - We believe they altered and possibly improved the design and began manufacturing their own Synthoid models in - order to bolster their criminal activities. -
-
- Your task is to infiltrate and destroy the {FactionName.Tetrads}' base of operations in Los Angeles. - Intelligence tells us that their base houses one of their Synthoid manufacturing units. - - ), - }, - [BlackOperationName.OperationK]: { - desc: ( - <> - CODE RED SITUATION. Our intelligence tells us that VitaLife has discovered a new android cloning technology. - This technology is supposedly capable of cloning Synthoids, not only physically but also their advanced AI - modules. We do not believe that VitaLife is trying to use this technology illegally or maliciously, but if any - Synthoids were able to infiltrate the corporation and take advantage of this technology then the results would - be catastrophic. -
-
- We do not have the power or jurisdiction to shut this down through legal or political means, so we must resort - to a covert operation. Your goal is to destroy this technology and eliminate anyone who was involved in its - creation. - - ), - }, - [BlackOperationName.OperationDeckard]: { - desc: ( - <> - Despite your success in eliminating VitaLife's new android-replicating technology in{" "} - {BlackOperationName.OperationK}, we've discovered that a small group of MK-VI Synthoids were able to make off - with the schematics and design of the technology before the Operation. It is almost a certainty that these - Synthoids are some of the rogue MK-VI ones from the Synthoid Uprising. -
-
- The goal of {BlackOperationName.OperationDeckard} is to hunt down these Synthoids and retire them. I don't need - to tell you how critical this mission is. - - ), - }, - [BlackOperationName.OperationTyrell]: { - desc: ( - <> - A week ago {FactionName.BladeIndustries} reported a small break-in at one of their {CityName.Aevum} Augmentation - storage facilities. We figured out that {FactionName.TheDarkArmy} was behind the heist, and didn't think any - more of it. However, we've just discovered that several known MK-VI Synthoids were part of that break-in group. -
-
- We cannot have Synthoids upgrading their already-enhanced abilities with Augmentations. Your task is to hunt - down associated {FactionName.TheDarkArmy} members and eliminate them. - - ), - }, - [BlackOperationName.OperationWallace]: { - desc: ( - <> - Based on information gathered from {BlackOperationName.OperationTyrell}, we've discovered that{" "} - {FactionName.TheDarkArmy} was well aware that there were Synthoids amongst their ranks. Even worse, we believe - that {FactionName.TheDarkArmy} is working together with other criminal organizations such as{" "} - {FactionName.TheSyndicate} and that they are planning some sort of large-scale takeover of multiple major - cities, most notably {CityName.Aevum}. We suspect that Synthoids have infiltrated the ranks of these criminal - factions and are trying to stage another Synthoid uprising. -
-
- The best way to deal with this is to prevent it before it even happens. The goal of{" "} - {BlackOperationName.OperationWallace} is to destroy {FactionName.TheDarkArmy} and Syndicate factions in{" "} - {CityName.Aevum} immediately. Leave no survivors. - - ), - }, - [BlackOperationName.OperationShoulderOfOrion]: { - desc: ( - <> - China's Solaris Space Systems is secretly launching the first manned spacecraft in over a decade using - Synthoids. We believe China is trying to establish the first off-world colonies. -
-
- The mission is to prevent this launch without instigating an international conflict. When you accept this - mission you will be officially disavowed by the NSA and the national government until after you successfully - return. In the event of failure, all of the operation's team members must not let themselves be captured alive. - - ), - }, - [BlackOperationName.OperationHyron]: { - desc: ( - <> - Our intelligence tells us that {FactionName.FulcrumSecretTechnologies} is developing a quantum supercomputer - using human brains as core processors. This supercomputer is rumored to be able to store vast amounts of data - and perform computations unmatched by any other supercomputer on the planet. But more importantly, the use of - organic human brains means that the supercomputer may be able to reason abstractly and become self-aware. -
-
- I do not need to remind you why sentient-level AIs pose a serious threat to all of mankind. -
-
- The research for this project is being conducted at one of {FactionName.FulcrumSecretTechnologies} secret - facilities in {CityName.Aevum}, codenamed 'Alpha Ranch'. Infiltrate the compound, delete and destroy the work, - and then find and kill the project lead. - - ), - }, - [BlackOperationName.OperationMorpheus]: { - desc: ( - <> - DreamSense Technologies is an advertising company that uses special technology to transmit their ads into the - people's dreams and subconscious. They do this using broadcast transmitter towers. Based on information from our - agents and informants in {CityName.Chongqing}, we have reason to believe that one of the broadcast towers there - has been compromised by Synthoids and is being used to spread pro-Synthoid propaganda. -
-
- The mission is to destroy this broadcast tower. Speed and stealth are of the utmost importance for this. - - ), - }, - [BlackOperationName.OperationIonStorm]: { - desc: ( - <> - Our analysts have uncovered a gathering of MK-VI Synthoids that have taken up residence in the{" "} - {CityName.Sector12} Slums. We don't know if they are rogue Synthoids from the Uprising, but we do know that they - have been stockpiling weapons, money, and other resources. This makes them dangerous. -
-
- This is a full-scale assault operation to find and retire all of these Synthoids in the {CityName.Sector12}{" "} - Slums. - - ), - }, - [BlackOperationName.OperationAnnihilus]: { - desc: ( - <> - Our superiors have ordered us to eradicate everything and everyone in an underground facility located in{" "} - {CityName.Aevum}. They tell us that the facility houses many dangerous Synthoids and belongs to a terrorist - organization called '{FactionName.TheCovenant}'. We have no prior intelligence about this organization, so you - are going in blind. - - ), - }, - [BlackOperationName.OperationUltron]: { - desc: ( - <> - {FactionName.OmniTekIncorporated}, the original designer and manufacturer of Synthoids, has notified us of a - malfunction in their AI design. This malfunction, when triggered, causes MK-VI Synthoids to become radicalized - and seek out the destruction of humanity. They say that this bug affects all MK-VI Synthoids, not just the rogue - ones from the Uprising. -
-
- {FactionName.OmniTekIncorporated} has also told us they believe someone has triggered this malfunction in a - large group of MK-VI Synthoids, and that these newly-radicalized Synthoids are now amassing in{" "} - {CityName.Volhaven} to form a terrorist group called Ultron. -
-
- Intelligence suggests Ultron is heavily armed and that their members are augmented. We believe Ultron is making - moves to take control of and weaponize DeltaOne's Tactical High-Energy Satellite Laser Array (THESLA). -
-
- Your task is to find and destroy Ultron. - - ), - }, - [BlackOperationName.OperationCenturion]: { - desc: ( - <> - {"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"} -
-
- Throughout all of humanity's history, we have relied on technology to survive, conquer, and progress. Its - advancement became our primary goal. And at the peak of human civilization technology turned into power. Global, - absolute power. -
-
- It seems that the universe is not without a sense of irony. -
-
- {"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"} - - ), - }, - [BlackOperationName.OperationVindictus]: { - desc: ( - <> - {"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"} -
-
- The bits are all around us. The daemons that hold the Node together can manifest themselves in many different - ways. -
-
- {"D)@#)($M)C0293c40($*)@#D0JUMP3Rm0C<*@#)*$)#02c94830c(#$*D)"} - - ), - }, - [BlackOperationName.OperationDaedalus]: { - desc: <> Yesterday we obeyed kings and bent our neck to emperors. Today we kneel only to truth., - }, -}; diff --git a/src/Bladeburner/data/Contracts.ts b/src/Bladeburner/data/Contracts.ts new file mode 100644 index 000000000..4c96c03a5 --- /dev/null +++ b/src/Bladeburner/data/Contracts.ts @@ -0,0 +1,121 @@ +import { BladeContractName } from "@enums"; +import { Contract } from "../Actions/Contract"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { assertLoadingType } from "../../utils/TypeAssertion"; + +export function createContracts(): Record { + return { + [BladeContractName.tracking]: new Contract({ + name: BladeContractName.tracking, + desc: + "Identify and locate Synthoids. This contract involves reconnaissance and information-gathering ONLY. Do NOT " + + "engage. Stealth is of the utmost importance.\n\n" + + "Successfully completing Tracking contracts will slightly improve your Synthoid population estimate for whatever " + + "city you are currently in.", + baseDifficulty: 125, + difficultyFac: 1.02, + rewardFac: 1.041, + rankGain: 0.3, + hpLoss: 0.5, + weights: { + hacking: 0, + strength: 0.05, + defense: 0.05, + dexterity: 0.35, + agility: 0.35, + charisma: 0.1, + intelligence: 0.05, + }, + decays: { + hacking: 0, + strength: 0.91, + defense: 0.91, + dexterity: 0.91, + agility: 0.91, + charisma: 0.9, + intelligence: 1, + }, + isStealth: true, + growthFunction: () => getRandomInt(5, 75) / 10, + minCount: 25, + }), + [BladeContractName.bountyHunter]: new Contract({ + name: BladeContractName.bountyHunter, + desc: + "Hunt down and capture fugitive Synthoids. These Synthoids are wanted alive.\n\n" + + "Successfully completing a Bounty Hunter contract will lower the population in your current city, and will also " + + "increase its chaos level.", + baseDifficulty: 250, + difficultyFac: 1.04, + rewardFac: 1.085, + rankGain: 0.9, + hpLoss: 1, + weights: { + hacking: 0, + strength: 0.15, + defense: 0.15, + dexterity: 0.25, + agility: 0.25, + charisma: 0.1, + intelligence: 0.1, + }, + decays: { + hacking: 0, + strength: 0.91, + defense: 0.91, + dexterity: 0.91, + agility: 0.91, + charisma: 0.8, + intelligence: 0.9, + }, + isKill: true, + growthFunction: () => getRandomInt(5, 75) / 10, + minCount: 5, + }), + [BladeContractName.retirement]: new Contract({ + name: BladeContractName.retirement, + desc: + "Hunt down and retire (kill) rogue Synthoids.\n\n" + + "Successfully completing a Retirement contract will lower the population in your current city, and will also " + + "increase its chaos level.", + baseDifficulty: 200, + difficultyFac: 1.03, + rewardFac: 1.065, + rankGain: 0.6, + hpLoss: 1, + weights: { + hacking: 0, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0.1, + intelligence: 0.1, + }, + decays: { + hacking: 0, + strength: 0.91, + defense: 0.91, + dexterity: 0.91, + agility: 0.91, + charisma: 0.8, + intelligence: 0.9, + }, + isKill: true, + growthFunction: () => getRandomInt(5, 75) / 10, + minCount: 5, + }), + }; +} + +export function loadContractsData(data: unknown, contracts: Record) { + // loading data as "unknown" and typechecking it down is probably not necessary + // but this will prevent crashes even with malformed savedata + if (!data || typeof data !== "object") return; + assertLoadingType>(data); + for (const contractName of Object.values(BladeContractName)) { + const loadedContract = data[contractName]; + if (!(loadedContract instanceof Contract)) continue; + contracts[contractName].loadData(loadedContract); + } +} diff --git a/src/Bladeburner/data/Contracts.tsx b/src/Bladeburner/data/Contracts.tsx deleted file mode 100644 index afc00ea4a..000000000 --- a/src/Bladeburner/data/Contracts.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react"; - -interface IContract { - desc: JSX.Element; -} - -export const Contracts: Record = { - 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. - - ), - }, - "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. - - ), - }, - 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. - - ), - }, -}; diff --git a/src/Bladeburner/data/GeneralActions.ts b/src/Bladeburner/data/GeneralActions.ts new file mode 100644 index 000000000..1a23ba6d2 --- /dev/null +++ b/src/Bladeburner/data/GeneralActions.ts @@ -0,0 +1,58 @@ +import { BladeGeneralActionName } from "@enums"; +import { GeneralAction } from "../Actions/GeneralAction"; +import { BladeburnerConstants } from "./Constants"; + +export const GeneralActions: Record = { + [BladeGeneralActionName.training]: new GeneralAction({ + name: BladeGeneralActionName.training, + getActionTime: () => 30, + desc: + "Improve your abilities at the Bladeburner unit's specialized training center. Doing this gives experience for " + + "all combat stats and also increases your max stamina.", + }), + [BladeGeneralActionName.fieldAnalysis]: new GeneralAction({ + name: BladeGeneralActionName.fieldAnalysis, + getActionTime: () => 30, + desc: + "Mine and analyze Synthoid-related data. This improves the Bladeburner unit's intelligence on Synthoid locations " + + "and activities. Completing this action will improve the accuracy of your Synthoid population estimated in the " + + "current city.\n\n" + + "Does NOT require stamina.", + }), + [BladeGeneralActionName.recruitment]: new GeneralAction({ + name: BladeGeneralActionName.recruitment, + getActionTime: function (bladeburner, person) { + const effCharisma = bladeburner.getEffectiveSkillLevel(person, "charisma"); + const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90; + return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor)); + }, + getSuccessChance: function (bladeburner, person) { + return Math.pow(person.skills.charisma, 0.45) / (bladeburner.teamSize - bladeburner.sleeveSize + 1); + }, + desc: + "Attempt to recruit members for your Bladeburner team. These members can help you conduct operations.\n\n" + + "Does NOT require stamina.", + }), + [BladeGeneralActionName.diplomacy]: new GeneralAction({ + name: BladeGeneralActionName.diplomacy, + getActionTime: () => 60, + desc: + "Improve diplomatic relations with the Synthoid population. Completing this action will reduce the Chaos level in " + + "your current city.\n\n" + + "Does NOT require stamina.", + }), + [BladeGeneralActionName.hyperbolicRegen]: new GeneralAction({ + name: BladeGeneralActionName.hyperbolicRegen, + getActionTime: () => 60, + desc: + "Enter cryogenic stasis using the Bladeburner division's hi-tech Regeneration Chamber. This will slowly heal your " + + "wounds and slightly increase your stamina.", + }), + [BladeGeneralActionName.inciteViolence]: new GeneralAction({ + name: BladeGeneralActionName.inciteViolence, + getActionTime: () => 60, + desc: + "Purposefully stir trouble in the synthoid community in order to gain a political edge. This will generate " + + "additional contracts and operations, at the cost of increased Chaos.", + }), +}; diff --git a/src/Bladeburner/data/GeneralActions.tsx b/src/Bladeburner/data/GeneralActions.tsx deleted file mode 100644 index 64c310a0c..000000000 --- a/src/Bladeburner/data/GeneralActions.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from "react"; -import { newWorkStats, WorkStats } from "../../Work/WorkStats"; - -interface IGeneral { - desc: JSX.Element; - exp: WorkStats; -} - -export const GeneralActions: Record = { - Training: { - desc: ( - <> - Improve your abilities at the Bladeburner unit's specialized training center. Doing this gives experience for - all combat stats and also increases your max stamina. - - ), - exp: newWorkStats({ - strExp: 30, - defExp: 30, - dexExp: 30, - agiExp: 30, - }), - }, - - "Field Analysis": { - desc: ( - <> - Mine and analyze Synthoid-related data. This improves the Bladeburner unit's intelligence on Synthoid locations - and activities. Completing this action will improve the accuracy of your Synthoid population estimated in the - current city. -
-
- Does NOT require stamina. - - ), - exp: newWorkStats({ - hackExp: 20, - chaExp: 20, - }), - }, - - Recruitment: { - desc: ( - <> - Attempt to recruit members for your Bladeburner team. These members can help you conduct operations. -
-
- Does NOT require stamina. - - ), - exp: newWorkStats({ - chaExp: 120, - }), - }, - - Diplomacy: { - desc: ( - <> - Improve diplomatic relations with the Synthoid population. Completing this action will reduce the Chaos level in - your current city. -
-
- Does NOT require stamina. - - ), - exp: newWorkStats({ - chaExp: 120, - }), - }, - - "Hyperbolic Regeneration Chamber": { - desc: ( - <> - Enter cryogenic stasis using the Bladeburner division's hi-tech Regeneration Chamber. This will slowly heal your - wounds and slightly increase your stamina. -
-
- - ), - exp: newWorkStats(), - }, - "Incite Violence": { - desc: ( - <> - Purposefully stir trouble in the synthoid community in order to gain a political edge. This will generate - additional contracts and operations, at the cost of increased Chaos. - - ), - exp: newWorkStats({ - strExp: 10, - defExp: 10, - dexExp: 10, - agiExp: 10, - chaExp: 10, - }), - }, -}; diff --git a/src/Bladeburner/data/Growths.ts b/src/Bladeburner/data/Growths.ts deleted file mode 100644 index c80968c2d..000000000 --- a/src/Bladeburner/data/Growths.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { getRandomInt } from "../../utils/helpers/getRandomInt"; - -export const Growths: { - [key: string]: (() => number) | undefined; - ["Tracking"]: () => number; - ["Bounty Hunter"]: () => number; - ["Retirement"]: () => number; - ["Investigation"]: () => number; - ["Undercover Operation"]: () => number; - ["Sting Operation"]: () => number; - ["Raid"]: () => number; - ["Stealth Retirement Operation"]: () => number; - ["Assassination"]: () => number; -} = { - Tracking: () => getRandomInt(5, 75) / 10, - "Bounty Hunter": () => getRandomInt(5, 75) / 10, - Retirement: () => getRandomInt(5, 75) / 10, - Investigation: () => getRandomInt(10, 40) / 10, - "Undercover Operation": () => getRandomInt(10, 40) / 10, - "Sting Operation": () => getRandomInt(3, 40) / 10, - Raid: () => getRandomInt(2, 40) / 10, - "Stealth Retirement Operation": () => getRandomInt(1, 20) / 10, - Assassination: () => getRandomInt(1, 20) / 10, -}; diff --git a/src/Bladeburner/data/Operations.ts b/src/Bladeburner/data/Operations.ts new file mode 100644 index 000000000..61bfdfdd9 --- /dev/null +++ b/src/Bladeburner/data/Operations.ts @@ -0,0 +1,220 @@ +import { BladeOperationName } from "@enums"; +import { Operation } from "../Actions/Operation"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { LevelableActionClass } from "../Actions/LevelableAction"; +import { assertLoadingType } from "../../utils/TypeAssertion"; + +export function createOperations(): Record { + return { + [BladeOperationName.investigation]: new Operation({ + name: BladeOperationName.investigation, + desc: + "As a field agent, investigate and identify Synthoid populations, movements, and operations.\n\n" + + "Successful Investigation ops will increase the accuracy of your synthoid data.\n\n" + + "You will NOT lose HP from failed Investigation ops.", + baseDifficulty: 400, + difficultyFac: 1.03, + rewardFac: 1.07, + rankGain: 2.2, + rankLoss: 0.2, + weights: { + hacking: 0.25, + strength: 0.05, + defense: 0.05, + dexterity: 0.2, + agility: 0.1, + charisma: 0.25, + intelligence: 0.1, + }, + decays: { + hacking: 0.85, + strength: 0.9, + defense: 0.9, + dexterity: 0.9, + agility: 0.9, + charisma: 0.7, + intelligence: 0.9, + }, + isStealth: true, + growthFunction: () => getRandomInt(10, 40) / 10, + maxCount: 100, + }), + [BladeOperationName.undercover]: new Operation({ + name: BladeOperationName.undercover, + desc: + "Conduct undercover operations to identify hidden and underground Synthoid communities and organizations.\n\n" + + "Successful Undercover ops will increase the accuracy of your synthoid data.", + baseDifficulty: 500, + difficultyFac: 1.04, + rewardFac: 1.09, + rankGain: 4.4, + rankLoss: 0.4, + hpLoss: 2, + weights: { + hacking: 0.2, + strength: 0.05, + defense: 0.05, + dexterity: 0.2, + agility: 0.2, + charisma: 0.2, + intelligence: 0.1, + }, + decays: { + hacking: 0.8, + strength: 0.9, + defense: 0.9, + dexterity: 0.9, + agility: 0.9, + charisma: 0.7, + intelligence: 0.9, + }, + isStealth: true, + growthFunction: () => getRandomInt(10, 40) / 10, + maxCount: 100, + }), + [BladeOperationName.sting]: new Operation({ + name: BladeOperationName.sting, + desc: "Conduct a sting operation to bait and capture particularly notorious Synthoid criminals.", + baseDifficulty: 650, + difficultyFac: 1.04, + rewardFac: 1.095, + rankGain: 5.5, + rankLoss: 0.5, + hpLoss: 2.5, + weights: { + hacking: 0.25, + strength: 0.05, + defense: 0.05, + dexterity: 0.25, + agility: 0.1, + charisma: 0.2, + intelligence: 0.1, + }, + decays: { + hacking: 0.8, + strength: 0.85, + defense: 0.85, + dexterity: 0.85, + agility: 0.85, + charisma: 0.7, + intelligence: 0.9, + }, + isStealth: true, + growthFunction: () => getRandomInt(3, 40) / 10, + }), + [BladeOperationName.raid]: new Operation({ + name: BladeOperationName.raid, + desc: + "Lead an assault on a known Synthoid community. Note that there must be an existing Synthoid community in your " + + "current city in order for this Operation to be successful.", + baseDifficulty: 800, + difficultyFac: 1.045, + rewardFac: 1.1, + rankGain: 55, + rankLoss: 2.5, + hpLoss: 50, + weights: { + hacking: 0.1, + strength: 0.2, + defense: 0.2, + dexterity: 0.2, + agility: 0.2, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.7, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.9, + }, + isKill: true, + growthFunction: () => getRandomInt(2, 40) / 10, + getAvailability: function (bladeburner) { + if (bladeburner.getCurrentCity().comms < 1) return { error: "No Synthoid communities in current city" }; + return LevelableActionClass.prototype.getAvailability.call(this, bladeburner); + }, + }), + [BladeOperationName.stealthRetirement]: new Operation({ + name: BladeOperationName.stealthRetirement, + desc: + "Lead a covert operation to retire Synthoids. The objective is to complete the task without drawing any " + + "attention. Stealth and discretion are key.", + baseDifficulty: 1000, + difficultyFac: 1.05, + rewardFac: 1.11, + rankGain: 22, + rankLoss: 2, + hpLoss: 10, + weights: { + hacking: 0.1, + strength: 0.1, + defense: 0.1, + dexterity: 0.3, + agility: 0.3, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.7, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.9, + }, + isStealth: true, + isKill: true, + growthFunction: () => getRandomInt(1, 20) / 10, + }), + [BladeOperationName.assassination]: new Operation({ + name: BladeOperationName.assassination, + desc: + "Assassinate Synthoids that have been identified as important, high-profile social and political leaders in the " + + "Synthoid communities.", + baseDifficulty: 1500, + difficultyFac: 1.06, + rewardFac: 1.14, + rankGain: 44, + rankLoss: 4, + hpLoss: 5, + weights: { + hacking: 0.1, + strength: 0.1, + defense: 0.1, + dexterity: 0.3, + agility: 0.3, + charisma: 0, + intelligence: 0.1, + }, + decays: { + hacking: 0.6, + strength: 0.8, + defense: 0.8, + dexterity: 0.8, + agility: 0.8, + charisma: 0, + intelligence: 0.8, + }, + isStealth: true, + isKill: true, + growthFunction: () => getRandomInt(1, 20) / 10, + }), + }; +} + +export function loadOperationsData(data: unknown, operations: Record) { + // loading data as "unknown" and typechecking it down is probably not necessary + // but this will prevent crashes even with malformed savedata + if (!data || typeof data !== "object") return; + assertLoadingType>(data); + for (const operationName of Object.values(BladeOperationName)) { + const loadedOperation = data[operationName]; + if (!(loadedOperation instanceof Operation)) continue; + operations[operationName].loadData(loadedOperation); + } +} diff --git a/src/Bladeburner/data/Operations.tsx b/src/Bladeburner/data/Operations.tsx deleted file mode 100644 index 77682f982..000000000 --- a/src/Bladeburner/data/Operations.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; - -interface IOperation { - desc: JSX.Element; -} - -export const Operations: Record = { - 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. - - ), - }, - "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. - - ), - }, - "Sting Operation": { - desc: <>Conduct a sting operation to bait and capture particularly notorious Synthoid criminals., - }, - Raid: { - desc: ( - <> - Lead an assault on a known Synthoid community. Note that there must be an existing Synthoid community in your - current city in order for this Operation to be successful. - - ), - }, - "Stealth Retirement Operation": { - desc: ( - <> - Lead a covert operation to retire Synthoids. The objective is to complete the task without drawing any - attention. Stealth and discretion are key. - - ), - }, - Assassination: { - desc: ( - <> - Assassinate Synthoids that have been identified as important, high-profile social and political leaders in the - Synthoid communities. - - ), - }, -}; diff --git a/src/Bladeburner/data/SkillNames.ts b/src/Bladeburner/data/SkillNames.ts deleted file mode 100644 index 7a0750633..000000000 --- a/src/Bladeburner/data/SkillNames.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const SkillNames = { - BladesIntuition: "Blade's Intuition", - Cloak: "Cloak", - Marksman: "Marksman", - WeaponProficiency: "Weapon Proficiency", - ShortCircuit: "Short-Circuit", - DigitalObserver: "Digital Observer", - Tracer: "Tracer", - Overclock: "Overclock", - Reaper: "Reaper", - EvasiveSystem: "Evasive System", - Datamancer: "Datamancer", - CybersEdge: "Cyber's Edge", - HandsOfMidas: "Hands of Midas", - Hyperdrive: "Hyperdrive", -}; diff --git a/src/Bladeburner/Skills.ts b/src/Bladeburner/data/Skills.ts similarity index 50% rename from src/Bladeburner/Skills.ts rename to src/Bladeburner/data/Skills.ts index df56154d2..9b4a8e561 100644 --- a/src/Bladeburner/Skills.ts +++ b/src/Bladeburner/data/Skills.ts @@ -1,78 +1,77 @@ -import { Skill } from "./Skill"; -import { SkillNames } from "./data/SkillNames"; +import { BladeMultName, BladeSkillName } from "@enums"; +import { Skill } from "../Skill"; -export const Skills: Record = {}; - -(function () { - Skills[SkillNames.BladesIntuition] = new Skill({ - name: SkillNames.BladesIntuition, +export const Skills: Record = { + [BladeSkillName.bladesIntuition]: new Skill({ + name: BladeSkillName.bladesIntuition, desc: "Each level of this skill increases your success chance for all Contracts, Operations, and BlackOps by 3%", baseCost: 3, costInc: 2.1, - successChanceAll: 3, - }); - Skills[SkillNames.Cloak] = new Skill({ - name: SkillNames.Cloak, + mults: { [BladeMultName.successChanceAll]: 3 }, + }), + [BladeSkillName.cloak]: new Skill({ + name: BladeSkillName.cloak, desc: "Each level of this skill increases your " + "success chance in stealth-related Contracts, Operations, and BlackOps by 5.5%", baseCost: 2, costInc: 1.1, - successChanceStealth: 5.5, - }); - Skills[SkillNames.ShortCircuit] = new Skill({ - name: SkillNames.ShortCircuit, + mults: { [BladeMultName.successChanceStealth]: 5.5 }, + }), + [BladeSkillName.shortCircuit]: new Skill({ + name: BladeSkillName.shortCircuit, desc: "Each level of this skill increases your success chance " + "in Contracts, Operations, and BlackOps that involve retirement by 5.5%", baseCost: 2, costInc: 2.1, - successChanceKill: 5.5, - }); - Skills[SkillNames.DigitalObserver] = new Skill({ - name: SkillNames.DigitalObserver, + mults: { [BladeMultName.successChanceKill]: 5.5 }, + }), + [BladeSkillName.digitalObserver]: new Skill({ + name: BladeSkillName.digitalObserver, desc: "Each level of this skill increases your success chance in all Operations and BlackOps by 4%", baseCost: 2, costInc: 2.1, - successChanceOperation: 4, - }); - Skills[SkillNames.Tracer] = new Skill({ - name: SkillNames.Tracer, + mults: { [BladeMultName.successChanceOperation]: 4 }, + }), + [BladeSkillName.tracer]: new Skill({ + name: BladeSkillName.tracer, desc: "Each level of this skill increases your success chance in all Contracts by 4%", baseCost: 2, costInc: 2.1, - successChanceContract: 4, - }); - Skills[SkillNames.Overclock] = new Skill({ - name: SkillNames.Overclock, + mults: { [BladeMultName.successChanceContract]: 4 }, + }), + [BladeSkillName.overclock]: new Skill({ + name: BladeSkillName.overclock, desc: "Each level of this skill decreases the time it takes " + "to attempt a Contract, Operation, and BlackOp by 1% (Max Level: 90)", baseCost: 3, costInc: 1.4, maxLvl: 90, - actionTime: 1, - }); - Skills[SkillNames.Reaper] = new Skill({ - name: SkillNames.Reaper, + mults: { [BladeMultName.actionTime]: -1 }, + }), + [BladeSkillName.reaper]: new Skill({ + name: BladeSkillName.reaper, desc: "Each level of this skill increases your effective combat stats for Bladeburner actions by 2%", baseCost: 2, costInc: 2.1, - effStr: 2, - effDef: 2, - effDex: 2, - effAgi: 2, - }); - Skills[SkillNames.EvasiveSystem] = new Skill({ - name: SkillNames.EvasiveSystem, + mults: { + [BladeMultName.effStr]: 2, + [BladeMultName.effDef]: 2, + [BladeMultName.effDex]: 2, + [BladeMultName.effAgi]: 2, + }, + }), + [BladeSkillName.evasiveSystem]: new Skill({ + name: BladeSkillName.evasiveSystem, desc: "Each level of this skill increases your effective dexterity and agility for Bladeburner actions by 4%", baseCost: 2, costInc: 2.1, - effDex: 4, - effAgi: 4, - }); - Skills[SkillNames.Datamancer] = new Skill({ - name: SkillNames.Datamancer, + mults: { [BladeMultName.effDex]: 4, [BladeMultName.effAgi]: 4 }, + }), + [BladeSkillName.datamancer]: new Skill({ + name: BladeSkillName.datamancer, desc: "Each level of this skill increases your effectiveness in " + "synthoid population analysis and investigation by 5%. " + @@ -80,27 +79,27 @@ export const Skills: Record = {}; "the accuracy of your synthoid population/community estimates.", baseCost: 3, costInc: 1, - successChanceEstimate: 5, - }); - Skills[SkillNames.CybersEdge] = new Skill({ - name: SkillNames.CybersEdge, + mults: { [BladeMultName.successChanceEstimate]: 5 }, + }), + [BladeSkillName.cybersEdge]: new Skill({ + name: BladeSkillName.cybersEdge, desc: "Each level of this skill increases your max stamina by 2%", baseCost: 1, costInc: 3, - stamina: 2, - }); - Skills[SkillNames.HandsOfMidas] = new Skill({ - name: SkillNames.HandsOfMidas, + mults: { [BladeMultName.stamina]: 2 }, + }), + [BladeSkillName.handsOfMidas]: new Skill({ + name: BladeSkillName.handsOfMidas, desc: "Each level of this skill increases the amount of money you receive from Contracts by 10%", baseCost: 2, costInc: 2.5, - money: 10, - }); - Skills[SkillNames.Hyperdrive] = new Skill({ - name: SkillNames.Hyperdrive, + mults: { [BladeMultName.money]: 10 }, + }), + [BladeSkillName.hyperdrive]: new Skill({ + name: BladeSkillName.hyperdrive, desc: "Each level of this skill increases the experience earned from Contracts, Operations, and BlackOps by 10%", baseCost: 1, costInc: 2.5, - expGain: 10, - }); -})(); + mults: { [BladeMultName.expGain]: 10 }, + }), +}; diff --git a/src/Bladeburner/ui/ActionLevel.tsx b/src/Bladeburner/ui/ActionLevel.tsx index 0187f853b..9210f565a 100644 --- a/src/Bladeburner/ui/ActionLevel.tsx +++ b/src/Bladeburner/ui/ActionLevel.tsx @@ -1,23 +1,21 @@ -import React from "react"; -import { Action } from "../Action"; -import { Bladeburner } from "../Bladeburner"; -import { BladeburnerConstants } from "../data/Constants"; +import type { Bladeburner } from "../Bladeburner"; +import type { LevelableAction } from "../Types"; -import Typography from "@mui/material/Typography"; -import Tooltip from "@mui/material/Tooltip"; -import Box from "@mui/material/Box"; -import IconButton from "@mui/material/IconButton"; +import React from "react"; +import { Box, IconButton, Tooltip, Typography } from "@mui/material"; import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; -interface IProps { - action: Action; +import { BladeburnerConstants } from "../data/Constants"; + +interface ActionLevelProps { + action: LevelableAction; isActive: boolean; bladeburner: Bladeburner; rerender: () => void; } -export function ActionLevel({ action, isActive, bladeburner, rerender }: IProps): React.ReactElement { +export function ActionLevel({ action, isActive, bladeburner, rerender }: ActionLevelProps): React.ReactElement { const canIncrease = action.level < action.maxLevel; const canDecrease = action.level > 1; diff --git a/src/Bladeburner/ui/AllPages.tsx b/src/Bladeburner/ui/AllPages.tsx index e6fa94cd6..0a21d9fd1 100644 --- a/src/Bladeburner/ui/AllPages.tsx +++ b/src/Bladeburner/ui/AllPages.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { Box, Tab, Tabs } from "@mui/material"; + import { GeneralActionPage } from "./GeneralActionPage"; import { ContractPage } from "./ContractPage"; import { OperationPage } from "./OperationPage"; @@ -6,15 +8,11 @@ import { BlackOpPage } from "./BlackOpPage"; import { SkillPage } from "./SkillPage"; import { Bladeburner } from "../Bladeburner"; -import Tabs from "@mui/material/Tabs"; -import Tab from "@mui/material/Tab"; -import Box from "@mui/material/Box"; - interface IProps { bladeburner: Bladeburner; } -export function AllPages(props: IProps): React.ReactElement { +export function AllPages({ bladeburner }: IProps): React.ReactElement { const [value, setValue] = React.useState(0); function handleChange(event: React.SyntheticEvent, tab: number): void { @@ -31,11 +29,11 @@ export function AllPages(props: IProps): React.ReactElement { - {value === 0 && } - {value === 1 && } - {value === 2 && } - {value === 3 && } - {value === 4 && } + {value === 0 && } + {value === 1 && } + {value === 2 && } + {value === 3 && } + {value === 4 && } ); diff --git a/src/Bladeburner/ui/Autolevel.tsx b/src/Bladeburner/ui/Autolevel.tsx index 6e18e64ee..82eae984b 100644 --- a/src/Bladeburner/ui/Autolevel.tsx +++ b/src/Bladeburner/ui/Autolevel.tsx @@ -1,26 +1,24 @@ -import React from "react"; -import { Action } from "../Action"; -import Typography from "@mui/material/Typography"; -import Tooltip from "@mui/material/Tooltip"; -import Box from "@mui/material/Box"; -import Switch from "@mui/material/Switch"; +import type { LevelableAction } from "../Types"; -interface IProps { - action: Action; +import React from "react"; +import { Box, Switch, Tooltip, Typography } from "@mui/material"; + +interface AutoLevelProps { + action: LevelableAction; rerender: () => void; } -export function Autolevel(props: IProps): React.ReactElement { +export function Autolevel({ action, rerender }: AutoLevelProps): React.ReactElement { function onAutolevel(event: React.ChangeEvent): void { - props.action.autoLevel = event.target.checked; - props.rerender(); + action.autoLevel = event.target.checked; + rerender(); } return ( Automatically increase operation level when possible}> Autolevel: - + ); } diff --git a/src/Bladeburner/ui/BlackOpElem.tsx b/src/Bladeburner/ui/BlackOpElem.tsx index d674af83b..ee7c703fe 100644 --- a/src/Bladeburner/ui/BlackOpElem.tsx +++ b/src/Bladeburner/ui/BlackOpElem.tsx @@ -1,93 +1,79 @@ import React from "react"; +import { Paper, Typography } from "@mui/material"; + +import { Player } from "@player"; import { formatNumberNoSuffix } from "../../ui/formatNumber"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; -import { ActionTypes } from "../data/ActionTypes"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { TeamSizeButton } from "./TeamSizeButton"; import { Bladeburner } from "../Bladeburner"; -import { BlackOperation } from "../BlackOperation"; -import { BlackOperations } from "../data/BlackOperations"; -import { Player } from "@player"; +import { BlackOperation } from "../Actions/BlackOperation"; import { CopyableText } from "../../ui/React/CopyableText"; import { SuccessChance } from "./SuccessChance"; import { StartButton } from "./StartButton"; - -import Typography from "@mui/material/Typography"; -import Paper from "@mui/material/Paper"; import { useRerender } from "../../ui/React/hooks"; -interface IProps { +interface BlackOpElemProps { bladeburner: Bladeburner; - action: BlackOperation; + blackOp: BlackOperation; } -export function BlackOpElem(props: IProps): React.ReactElement { +export function BlackOpElem({ bladeburner, blackOp }: BlackOpElemProps): React.ReactElement { const rerender = useRerender(); - const isCompleted = props.bladeburner.blackops[props.action.name] != null; + const isCompleted = bladeburner.numBlackOpsComplete > blackOp.n; if (isCompleted) { return ( - {props.action.name} (COMPLETED) + {blackOp.name} (COMPLETED) ); } - const isActive = - props.bladeburner.action.type === ActionTypes.BlackOperation && props.action.name === props.bladeburner.action.name; - const actionTime = props.action.getActionTime(props.bladeburner, Player); - const hasReqdRank = props.bladeburner.rank >= props.action.reqdRank; + const isActive = bladeburner.action?.name === blackOp.name; + const actionTime = blackOp.getActionTime(bladeburner, Player); + const hasReqdRank = bladeburner.rank >= blackOp.reqdRank; const computedActionTimeCurrent = Math.min( - props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, - props.bladeburner.actionTimeToComplete, + bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow, + bladeburner.actionTimeToComplete, ); - const actionData = BlackOperations[props.action.name]; - if (actionData === undefined) { - throw new Error(`Cannot find data for ${props.action.name}`); - } - return ( {isActive ? ( <> <> - + (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} - {formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)}) + {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)}) {createProgressBarText({ - progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, + progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete, })} ) : ( <> - + - - + + )}

- {actionData.desc} + {blackOp.desc}

- Required Rank: {formatNumberNoSuffix(props.action.reqdRank, 0)} + Required Rank: {formatNumberNoSuffix(blackOp.reqdRank, 0)}
- +
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
diff --git a/src/Bladeburner/ui/BlackOpList.tsx b/src/Bladeburner/ui/BlackOpList.tsx index cebee7909..ece42b71f 100644 --- a/src/Bladeburner/ui/BlackOpList.tsx +++ b/src/Bladeburner/ui/BlackOpList.tsx @@ -1,39 +1,20 @@ import React from "react"; -import { BlackOperations } from "../BlackOperations"; -import { BlackOperation } from "../BlackOperation"; +import { blackOpsArray } from "../data/BlackOperations"; +import { BlackOperation } from "../Actions/BlackOperation"; import { BlackOpElem } from "./BlackOpElem"; import { Bladeburner } from "../Bladeburner"; -interface IProps { +interface BlackOpListProps { bladeburner: Bladeburner; } -export function BlackOpList(props: IProps): React.ReactElement { - let blackops: BlackOperation[] = []; - for (const blackopName of Object.keys(BlackOperations)) { - if (Object.hasOwn(BlackOperations, blackopName)) { - blackops.push(BlackOperations[blackopName]); - } - } - blackops.sort(function (a, b) { - return a.reqdRank - b.reqdRank; - }); - - blackops = blackops.filter( - (blackop: BlackOperation, i: number) => - !( - props.bladeburner.blackops[blackops[i].name] == null && - i !== 0 && - props.bladeburner.blackops[blackops[i - 1].name] == null - ), - ); - - blackops = blackops.reverse(); +export function BlackOpList({ bladeburner }: BlackOpListProps): React.ReactElement { + const blackOps = blackOpsArray.slice(0, bladeburner.numBlackOpsComplete + 1); return ( <> - {blackops.map((blackop: BlackOperation) => ( - + {blackOps.map((blackOp: BlackOperation) => ( + ))} ); diff --git a/src/Bladeburner/ui/BlackOpPage.tsx b/src/Bladeburner/ui/BlackOpPage.tsx index 3426dde7d..9335d095e 100644 --- a/src/Bladeburner/ui/BlackOpPage.tsx +++ b/src/Bladeburner/ui/BlackOpPage.tsx @@ -1,17 +1,18 @@ import * as React from "react"; import { Button, Typography } from "@mui/material"; -import { BlackOperationName, FactionName } from "@enums"; +import { FactionName } from "@enums"; import { BlackOpList } from "./BlackOpList"; import { Bladeburner } from "../Bladeburner"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; import { CorruptableText } from "../../ui/React/CorruptableText"; +import { blackOpsArray } from "../data/BlackOperations"; -interface IProps { +interface BlackOpPageProps { bladeburner: Bladeburner; } -export function BlackOpPage(props: IProps): React.ReactElement { +export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactElement { return ( <> @@ -28,12 +29,12 @@ export function BlackOpPage(props: IProps): React.ReactElement { Like normal operations, you may use a team for Black Ops. Failing a black op will incur heavy HP and rank losses. - {props.bladeburner.blackops[BlackOperationName.OperationDaedalus] ? ( + {bladeburner.numBlackOpsComplete >= blackOpsArray.length ? ( ) : ( - + )} ); diff --git a/src/Bladeburner/ui/ContractElem.tsx b/src/Bladeburner/ui/ContractElem.tsx index 613a5bffe..344d7f4ea 100644 --- a/src/Bladeburner/ui/ContractElem.tsx +++ b/src/Bladeburner/ui/ContractElem.tsx @@ -1,10 +1,7 @@ import React from "react"; -import { ActionTypes } from "../data/ActionTypes"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; -import { Contracts } from "../data/Contracts"; import { Bladeburner } from "../Bladeburner"; -import { Action } from "../Action"; import { Player } from "@player"; import { SuccessChance } from "./SuccessChance"; import { CopyableText } from "../../ui/React/CopyableText"; @@ -15,74 +12,64 @@ import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber"; import Typography from "@mui/material/Typography"; import Paper from "@mui/material/Paper"; import { useRerender } from "../../ui/React/hooks"; +import { Contract } from "../Actions/Contract"; +import { getEnumHelper } from "../../utils/EnumHelper"; -interface IProps { +interface ContractElemProps { bladeburner: Bladeburner; - action: Action; + action: Contract; } -export function ContractElem(props: IProps): React.ReactElement { +export function ContractElem({ bladeburner, action }: ContractElemProps): React.ReactElement { const rerender = useRerender(); - const isActive = - props.bladeburner.action.type === ActionTypes.Contract && props.action.name === props.bladeburner.action.name; + // Temp special return + if (!getEnumHelper("BladeContractName").isMember(action.name)) return <>; + const isActive = action.name === bladeburner.action?.name; const computedActionTimeCurrent = Math.min( - props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, - props.bladeburner.actionTimeToComplete, + bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow, + bladeburner.actionTimeToComplete, ); - const actionTime = props.action.getActionTime(props.bladeburner, Player); - - const actionData = Contracts[props.action.name]; - if (actionData === undefined) { - throw new Error(`Cannot find data for ${props.action.name}`); - } + const actionTime = action.getActionTime(bladeburner, Player); return ( {isActive ? ( <> - - (IN PROGRESS -{" "} - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} - {formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)}) - + (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} + {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)}) {createProgressBarText({ - progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, + progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete, })} ) : ( <> - - + + )}

- +

- - {actionData.desc} + + {action.desc}

- +
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
- Contracts remaining: {formatBigNumber(Math.floor(props.action.count))} + Contracts remaining: {formatBigNumber(Math.floor(action.count))}
- Successes: {formatBigNumber(props.action.successes)} + Successes: {formatBigNumber(action.successes)}
- Failures: {formatBigNumber(props.action.failures)} + Failures: {formatBigNumber(action.failures)}

- +
); } diff --git a/src/Bladeburner/ui/ContractList.tsx b/src/Bladeburner/ui/ContractList.tsx index 689238495..98189245f 100644 --- a/src/Bladeburner/ui/ContractList.tsx +++ b/src/Bladeburner/ui/ContractList.tsx @@ -1,18 +1,14 @@ import React from "react"; +import { BladeContractName } from "@enums"; import { ContractElem } from "./ContractElem"; import { Bladeburner } from "../Bladeburner"; -interface IProps { - bladeburner: Bladeburner; -} - -export function ContractList(props: IProps): React.ReactElement { - const names = Object.keys(props.bladeburner.contracts); - const contracts = props.bladeburner.contracts; +export function ContractList({ bladeburner }: { bladeburner: Bladeburner }): React.ReactElement { + const names = Object.values(BladeContractName); return ( <> - {names.map((name: string) => ( - + {names.map((name) => ( + ))} ); diff --git a/src/Bladeburner/ui/GeneralActionElem.tsx b/src/Bladeburner/ui/GeneralActionElem.tsx index 240c299c9..4821c2337 100644 --- a/src/Bladeburner/ui/GeneralActionElem.tsx +++ b/src/Bladeburner/ui/GeneralActionElem.tsx @@ -1,11 +1,10 @@ +import type { GeneralAction } from "../Actions/GeneralAction"; + import React from "react"; -import { ActionTypes } from "../data/ActionTypes"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { formatNumberNoSuffix } from "../../ui/formatNumber"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { Bladeburner } from "../Bladeburner"; -import { Action } from "../Action"; -import { GeneralActions } from "../data/GeneralActions"; import { Player } from "@player"; import { CopyableText } from "../../ui/React/CopyableText"; @@ -16,71 +15,46 @@ import Box from "@mui/material/Box"; import Paper from "@mui/material/Paper"; import { useRerender } from "../../ui/React/hooks"; -interface IProps { +interface GeneralActionElemProps { bladeburner: Bladeburner; - action: Action; + action: GeneralAction; } -export function GeneralActionElem(props: IProps): React.ReactElement { +export function GeneralActionElem({ bladeburner, action }: GeneralActionElemProps): React.ReactElement { const rerender = useRerender(); - const isActive = props.action.name === props.bladeburner.action.name; + const isActive = action.name === bladeburner.action?.name; const computedActionTimeCurrent = Math.min( - props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, - props.bladeburner.actionTimeToComplete, + bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow, + bladeburner.actionTimeToComplete, ); - const actionTime = (function (): number { - switch (props.action.name) { - case "Training": - case "Field Analysis": - return 30; - case "Diplomacy": - case "Hyperbolic Regeneration Chamber": - case "Incite Violence": - return 60; - case "Recruitment": - return props.bladeburner.getRecruitmentTime(Player); - } - return -1; // dead code - })(); + const actionTime = action.getActionTime(bladeburner, Player); const successChance = - props.action.name === "Recruitment" - ? Math.max(0, Math.min(props.bladeburner.getRecruitmentSuccessChance(Player), 1)) - : -1; - - const actionData = GeneralActions[props.action.name]; - if (actionData === undefined) { - throw new Error(`Cannot find data for ${props.action.name}`); - } + action.name === "Recruitment" ? Math.max(0, Math.min(bladeburner.getRecruitmentSuccessChance(Player), 1)) : -1; return ( {isActive ? ( <> - + (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} - {formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)}) + {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)}) {createProgressBarText({ - progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, + progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete, })} ) : ( - - + + )}

- {actionData.desc} + {action.desc}

diff --git a/src/Bladeburner/ui/GeneralActionList.tsx b/src/Bladeburner/ui/GeneralActionList.tsx index 442a0cb05..5e6b0fa7c 100644 --- a/src/Bladeburner/ui/GeneralActionList.tsx +++ b/src/Bladeburner/ui/GeneralActionList.tsx @@ -1,24 +1,18 @@ import React from "react"; import { GeneralActionElem } from "./GeneralActionElem"; -import { Action } from "../Action"; -import { GeneralActions } from "../GeneralActions"; +import { GeneralActions } from "../data/GeneralActions"; import { Bladeburner } from "../Bladeburner"; -interface IProps { +interface GeneralActionListProps { bladeburner: Bladeburner; } -export function GeneralActionList(props: IProps): React.ReactElement { - const actions: Action[] = []; - for (const name of Object.keys(GeneralActions)) { - if (Object.hasOwn(GeneralActions, name)) { - actions.push(GeneralActions[name]); - } - } +export function GeneralActionList({ bladeburner }: GeneralActionListProps): React.ReactElement { + const actions = Object.values(GeneralActions); return ( <> - {actions.map((action: Action) => ( - + {actions.map((action) => ( + ))} ); diff --git a/src/Bladeburner/ui/OperationElem.tsx b/src/Bladeburner/ui/OperationElem.tsx index cc5e5251f..487456410 100644 --- a/src/Bladeburner/ui/OperationElem.tsx +++ b/src/Bladeburner/ui/OperationElem.tsx @@ -1,5 +1,10 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { Operation } from "../Actions/Operation"; + import React from "react"; -import { ActionTypes } from "../data/ActionTypes"; +import { Paper, Typography } from "@mui/material"; + +import { Player } from "@player"; import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { SuccessChance } from "./SuccessChance"; @@ -7,85 +12,69 @@ import { ActionLevel } from "./ActionLevel"; import { Autolevel } from "./Autolevel"; import { StartButton } from "./StartButton"; import { TeamSizeButton } from "./TeamSizeButton"; -import { Bladeburner } from "../Bladeburner"; -import { Operation } from "../Operation"; -import { Operations } from "../data/Operations"; -import { Player } from "@player"; import { CopyableText } from "../../ui/React/CopyableText"; import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber"; -import Typography from "@mui/material/Typography"; -import Paper from "@mui/material/Paper"; import { useRerender } from "../../ui/React/hooks"; +import { BladeActionType } from "@enums"; -interface IProps { +interface OperationElemProps { bladeburner: Bladeburner; - action: Operation; + operation: Operation; } -export function OperationElem(props: IProps): React.ReactElement { +export function OperationElem({ bladeburner, operation }: OperationElemProps): React.ReactElement { const rerender = useRerender(); const isActive = - props.bladeburner.action.type === ActionTypes.Operation && props.action.name === props.bladeburner.action.name; + bladeburner.action?.type === BladeActionType.operation && operation.name === bladeburner.action?.name; const computedActionTimeCurrent = Math.min( - props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, - props.bladeburner.actionTimeToComplete, + bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow, + bladeburner.actionTimeToComplete, ); - const actionTime = props.action.getActionTime(props.bladeburner, Player); - - const actionData = Operations[props.action.name]; - if (actionData === undefined) { - throw new Error(`Cannot find data for ${props.action.name}`); - } + const actionTime = operation.getActionTime(bladeburner, Player); return ( {isActive ? ( <> - (IN PROGRESS -{" "} - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "} - {formatNumberNoSuffix(props.bladeburner.actionTimeToComplete, 0)}) + (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)}{" "} + / {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)}) {createProgressBarText({ - progress: computedActionTimeCurrent / props.bladeburner.actionTimeToComplete, + progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete, })} ) : ( <> - - - + + + )}

- +

- - {actionData.desc} + + {operation.desc}

- +
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
- Operations remaining: {formatBigNumber(Math.floor(props.action.count))} + Operations remaining: {formatBigNumber(Math.floor(operation.count))}
- Successes: {formatBigNumber(props.action.successes)} + Successes: {formatBigNumber(operation.successes)}
- Failures: {formatBigNumber(props.action.failures)} + Failures: {formatBigNumber(operation.failures)}

- +
); } diff --git a/src/Bladeburner/ui/OperationList.tsx b/src/Bladeburner/ui/OperationList.tsx index 610896b6c..a3d62bd0e 100644 --- a/src/Bladeburner/ui/OperationList.tsx +++ b/src/Bladeburner/ui/OperationList.tsx @@ -1,18 +1,14 @@ import React from "react"; +import { BladeOperationName } from "@enums"; import { OperationElem } from "./OperationElem"; import { Bladeburner } from "../Bladeburner"; -interface IProps { - bladeburner: Bladeburner; -} - -export function OperationList(props: IProps): React.ReactElement { - const names = Object.keys(props.bladeburner.operations); - const operations = props.bladeburner.operations; +export function OperationList({ bladeburner }: { bladeburner: Bladeburner }): React.ReactElement { + const names = Object.values(BladeOperationName); return ( <> - {names.map((name: string) => ( - + {names.map((name) => ( + ))} ); diff --git a/src/Bladeburner/ui/SkillElem.tsx b/src/Bladeburner/ui/SkillElem.tsx index 2f7181a6e..56dffe03b 100644 --- a/src/Bladeburner/ui/SkillElem.tsx +++ b/src/Bladeburner/ui/SkillElem.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { CopyableText } from "../../ui/React/CopyableText"; import { formatBigNumber } from "../../ui/formatNumber"; import { Bladeburner } from "../Bladeburner"; @@ -10,34 +10,29 @@ import AddIcon from "@mui/icons-material/Add"; import CloseIcon from "@mui/icons-material/Close"; import { Skill } from "../Skill"; -interface IProps { +interface SkillElemProps { skill: Skill; bladeburner: Bladeburner; 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); +export function SkillElem({ skill, bladeburner, onUpgrade }: SkillElemProps): React.ReactElement { + const skillName = skill.name; + const skillLevel = bladeburner.getSkillLevel(skillName); + const pointCost = useMemo(() => skill.calculateCost(skillLevel), [skill, skillLevel]); - const canLevel = props.bladeburner.skillPoints >= pointCost; - const maxLvl = props.skill.maxLvl ? currentLevel >= props.skill.maxLvl : false; + const canLevel = bladeburner.skillPoints >= pointCost; + const maxLvl = skill.maxLvl ? skillLevel >= skill.maxLvl : false; function onClick(): void { - if (props.bladeburner.skillPoints < pointCost) return; - props.bladeburner.skillPoints -= pointCost; - props.bladeburner.upgradeSkill(props.skill); - props.onUpgrade(); + bladeburner.upgradeSkill(skillName); + onUpgrade(); } return ( - + {!canLevel || maxLvl ? ( @@ -48,13 +43,13 @@ export function SkillElem(props: IProps): React.ReactElement { )} - Level: {formatBigNumber(currentLevel)} + Level: {formatBigNumber(skillLevel)} {maxLvl ? ( MAX LEVEL ) : ( Skill Points required: {formatBigNumber(pointCost)} )} - {props.skill.desc} + {skill.desc} ); } diff --git a/src/Bladeburner/ui/SkillList.tsx b/src/Bladeburner/ui/SkillList.tsx deleted file mode 100644 index f28b56fc2..000000000 --- a/src/Bladeburner/ui/SkillList.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from "react"; -import { SkillElem } from "./SkillElem"; -import { Skills } from "../Skills"; -import { Bladeburner } from "../Bladeburner"; - -interface IProps { - bladeburner: Bladeburner; - onUpgrade: () => void; -} - -export function SkillList(props: IProps): React.ReactElement { - return ( - <> - {Object.keys(Skills).map((skill: string) => ( - - ))} - - ); -} diff --git a/src/Bladeburner/ui/SkillPage.tsx b/src/Bladeburner/ui/SkillPage.tsx index e88251ea8..98fbdfab1 100644 --- a/src/Bladeburner/ui/SkillPage.tsx +++ b/src/Bladeburner/ui/SkillPage.tsx @@ -1,25 +1,24 @@ -import React, { useState } from "react"; -import { SkillList } from "./SkillList"; +import React from "react"; import { BladeburnerConstants } from "../data/Constants"; import { Bladeburner } from "../Bladeburner"; import { formatBigNumber } from "../../ui/formatNumber"; import Typography from "@mui/material/Typography"; -interface IProps { +import { useRerender } from "../../ui/React/hooks"; +import { SkillElem } from "./SkillElem"; +import { Skills } from "../data/Skills"; + +interface SkillPageProps { bladeburner: Bladeburner; } -export function SkillPage(props: IProps): React.ReactElement { - const setRerender = useState(false)[1]; - const mults = props.bladeburner.skillMultipliers; - - function valid(mult: number | undefined): boolean { - return mult !== undefined && mult !== 1; - } +export function SkillPage({ bladeburner }: SkillPageProps): React.ReactElement { + const rerender = useRerender(); + const multDisplays = bladeburner.getSkillMultsDisplay(); return ( <> - Skill Points: {formatBigNumber(props.bladeburner.skillPoints)} + Skill Points: {formatBigNumber(bladeburner.skillPoints)} You will gain one skill point every {BladeburnerConstants.RanksPerSkillPoint} ranks. @@ -27,53 +26,12 @@ export function SkillPage(props: IProps): React.ReactElement { Note that when upgrading a skill, the benefit for that skill is additive. However, the effects of different skills with each other is multiplicative. - {valid(mults.successChanceAll) && ( - Total Success Chance: x{formatBigNumber(mults.successChanceAll)} - )} - {valid(mults.successChanceStealth) && ( - Stealth Success Chance: x{formatBigNumber(mults.successChanceStealth)} - )} - {valid(mults.successChanceKill) && ( - Retirement Success Chance: x{formatBigNumber(mults.successChanceKill)} - )} - {valid(mults.successChanceContract) && ( - Contract Success Chance: x{formatBigNumber(mults.successChanceContract)} - )} - {valid(mults.successChanceOperation) && ( - Operation Success Chance: x{formatBigNumber(mults.successChanceOperation)} - )} - {valid(mults.successChanceEstimate) && ( - Synthoid Data Estimate: x{formatBigNumber(mults.successChanceEstimate)} - )} - {valid(mults.actionTime) && Action Time: x{formatBigNumber(mults.actionTime)}} - {valid(mults.effHack) && Hacking Skill: x{formatBigNumber(mults.effHack)}} - {valid(mults.effStr) && Strength: x{formatBigNumber(mults.effStr)}} - {valid(mults.effDef) && Defense: x{formatBigNumber(mults.effDef)}} - {valid(mults.effDex) && Dexterity: x{formatBigNumber(mults.effDex)}} - {valid(mults.effAgi) && Agility: x{formatBigNumber(mults.effAgi)}} - {valid(mults.effCha) && Charisma: x{formatBigNumber(mults.effCha)}} - {valid(mults.effInt) && Intelligence: x{formatBigNumber(mults.effInt)}} - {valid(mults.stamina) && Stamina: x{formatBigNumber(mults.stamina)}} - {valid(mults.money) && Contract Money: x{formatBigNumber(mults.money)}} - {valid(mults.expGain) && Exp Gain: x{formatBigNumber(mults.expGain)}} - setRerender((old) => !old)} /> + {multDisplays.map((multDisplay, i) => ( + {multDisplay} + ))} + {Object.values(Skills).map((skill) => ( + + ))} ); } - -/* - - - - -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]) { - - } - } -} -*/ diff --git a/src/Bladeburner/ui/StartButton.tsx b/src/Bladeburner/ui/StartButton.tsx index b472af4d2..d307ed4db 100644 --- a/src/Bladeburner/ui/StartButton.tsx +++ b/src/Bladeburner/ui/StartButton.tsx @@ -1,47 +1,27 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { Action } from "../Types"; + import React from "react"; +import { ButtonWithTooltip } from "../../ui/Components/ButtonWithTooltip"; -import { Bladeburner } from "../Bladeburner"; -import { BlackOperation } from "../BlackOperation"; -import { Player } from "@player"; -import Button from "@mui/material/Button"; -import { AugmentationName } from "@enums"; -import { ActionIdentifier } from "../ActionIdentifier"; - -interface IProps { +interface StartButtonProps { bladeburner: Bladeburner; - type: number; - name: string; + action: Action; rerender: () => void; } -export function StartButton(props: IProps): React.ReactElement { - const action = props.bladeburner.getActionObject(new ActionIdentifier({ name: props.name, type: props.type })); - if (action == null) { - throw new Error("Failed to get Operation Object for: " + props.name); - } - let disabled = false; - if (action.count < 1) { - disabled = true; - } - if (props.name === "Raid" && props.bladeburner.getCurrentCity().comms === 0) { - disabled = true; - } +export function StartButton({ bladeburner, action, rerender }: StartButtonProps): React.ReactElement { + const availability = action.getAvailability(bladeburner); + const disabledReason = availability.available ? "" : availability.error; - if (action instanceof BlackOperation && props.bladeburner.rank < action.reqdRank) { - disabled = true; - } function onStart(): void { - if (disabled) return; - const action = new ActionIdentifier(); - action.type = props.type; - action.name = props.name; - if (!Player.hasAugmentation(AugmentationName.BladesSimulacrum, true)) Player.finishWork(true); - props.bladeburner.startAction(action); - props.rerender(); + if (disabledReason) return; + bladeburner.startAction(action.id); + rerender(); } return ( - + ); } diff --git a/src/Bladeburner/ui/Stats.tsx b/src/Bladeburner/ui/Stats.tsx index 5faf7b77b..4a0ee786a 100644 --- a/src/Bladeburner/ui/Stats.tsx +++ b/src/Bladeburner/ui/Stats.tsx @@ -10,29 +10,23 @@ import { formatNumberNoSuffix, formatPopulation, formatBigNumber } from "../../u import { Factions } from "../../Faction/Factions"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; -import { joinFaction } from "../../Faction/FactionHelpers"; import { Bladeburner } from "../Bladeburner"; import { TravelModal } from "./TravelModal"; -interface IProps { +interface StatsProps { bladeburner: Bladeburner; } -export function Stats(props: IProps): React.ReactElement { +export function Stats({ bladeburner }: StatsProps): React.ReactElement { const [travelOpen, setTravelOpen] = useState(false); useRerender(1000); - const inFaction = props.bladeburner.rank >= BladeburnerConstants.RankNeededForFaction; + const inFaction = bladeburner.rank >= BladeburnerConstants.RankNeededForFaction; function openFaction(): void { - if (!inFaction) return; - const faction = Factions[FactionName.Bladeburners]; - if (!faction.isMember) { - joinFaction(faction); - } - - Router.toPage(Page.Faction, { faction }); + const success = bladeburner.joinFaction(); + if (success) Router.toPage(Page.Faction, { faction: Factions[FactionName.Bladeburners] }); } return ( @@ -49,11 +43,11 @@ export function Stats(props: IProps): React.ReactElement { - setTravelOpen(false)} bladeburner={props.bladeburner} /> + setTravelOpen(false)} bladeburner={bladeburner} /> Your rank within the Bladeburner division.
}> - Rank: {formatBigNumber(props.bladeburner.rank)} + Rank: {formatBigNumber(bladeburner.rank)}
@@ -82,23 +76,23 @@ export function Stats(props: IProps): React.ReactElement { } > - Stamina: {formatBigNumber(props.bladeburner.stamina)} / {formatBigNumber(props.bladeburner.maxStamina)} + Stamina: {formatBigNumber(bladeburner.stamina)} / {formatBigNumber(bladeburner.maxStamina)} - Stamina Penalty: {formatNumberNoSuffix((1 - props.bladeburner.calculateStaminaPenalty()) * 100, 1)}% + Stamina Penalty: {formatNumberNoSuffix((1 - bladeburner.calculateStaminaPenalty()) * 100, 1)}%
- Team Size: {formatNumberNoSuffix(props.bladeburner.teamSize, 0)} - Team Members Lost: {formatNumberNoSuffix(props.bladeburner.teamLost, 0)} + Team Size: {formatNumberNoSuffix(bladeburner.teamSize, 0)} + Team Members Lost: {formatNumberNoSuffix(bladeburner.teamLost, 0)}
- Num Times Hospitalized: {props.bladeburner.numHosp} + Num Times Hospitalized: {bladeburner.numHosp} - Money Lost From Hospitalizations: + Money Lost From Hospitalizations:
- Current City: {props.bladeburner.city} + Current City: {bladeburner.city} } > - - Est. Synthoid Population: {formatPopulation(props.bladeburner.getCurrentCity().popEst)} - + Est. Synthoid Population: {formatPopulation(bladeburner.getCurrentCity().popEst)} @@ -122,9 +114,7 @@ export function Stats(props: IProps): React.ReactElement { } > - - Synthoid Communities: {formatNumberNoSuffix(props.bladeburner.getCurrentCity().comms, 0)} - + Synthoid Communities: {formatNumberNoSuffix(bladeburner.getCurrentCity().comms, 0)} @@ -136,11 +126,11 @@ export function Stats(props: IProps): React.ReactElement { } > - City Chaos: {formatBigNumber(props.bladeburner.getCurrentCity().chaos)} + City Chaos: {formatBigNumber(bladeburner.getCurrentCity().chaos)}
- {(props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000 > 15000 && ( + {(bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000 > 15000 && ( <> Bonus time:{" "} {convertTimeMsToTimeElapsedString( - (props.bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000, + (bladeburner.storedCycles / BladeburnerConstants.CyclesPerSecond) * 1000, )} @@ -162,7 +152,7 @@ export function Stats(props: IProps): React.ReactElement {
)} - Skill Points: {formatBigNumber(props.bladeburner.skillPoints)} + Skill Points: {formatBigNumber(bladeburner.skillPoints)}
Aug. Success Chance mult: {formatNumberNoSuffix(Player.mults.bladeburner_success_chance * 100, 1)}% diff --git a/src/Bladeburner/ui/StealthIcon.tsx b/src/Bladeburner/ui/StealthIcon.tsx index d0613cc8e..43ac7af74 100644 --- a/src/Bladeburner/ui/StealthIcon.tsx +++ b/src/Bladeburner/ui/StealthIcon.tsx @@ -1,8 +1,7 @@ import React from "react"; import { stealthIcon } from "../data/Icons"; -import Typography from "@mui/material/Typography"; -import Tooltip from "@mui/material/Tooltip"; +import { Tooltip, Typography } from "@mui/material"; export function StealthIcon(): React.ReactElement { return This action involves stealth}>{stealthIcon}; diff --git a/src/Bladeburner/ui/SuccessChance.tsx b/src/Bladeburner/ui/SuccessChance.tsx index b6bd960d9..c6e7f204c 100644 --- a/src/Bladeburner/ui/SuccessChance.tsx +++ b/src/Bladeburner/ui/SuccessChance.tsx @@ -1,35 +1,27 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { Action } from "../Types"; + import React from "react"; -import { formatNumberNoSuffix } from "../../ui/formatNumber"; + +import { Player } from "@player"; +import { formatPercent } from "../../ui/formatNumber"; import { StealthIcon } from "./StealthIcon"; import { KillIcon } from "./KillIcon"; -import { Action } from "../Action"; -import { Bladeburner } from "../Bladeburner"; -import { Player } from "@player"; -interface IProps { +interface SuccessChanceProps { bladeburner: Bladeburner; action: Action; } -export function SuccessChance(props: IProps): React.ReactElement { - const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner, Player); +export function SuccessChance({ bladeburner, action }: SuccessChanceProps): React.ReactElement { + const [minChance, maxChance] = action.getSuccessRange(bladeburner, Player); - let chance = <>; - if (estimatedSuccessChance[0] === estimatedSuccessChance[1]) { - chance = <>{formatNumberNoSuffix(estimatedSuccessChance[0] * 100, 1)}%; - } else { - chance = ( - <> - {formatNumberNoSuffix(estimatedSuccessChance[0] * 100, 1)}% ~{" "} - {formatNumberNoSuffix(estimatedSuccessChance[1] * 100, 1)}% - - ); - } + const chance = formatPercent(minChance, 1) + (minChance === maxChance ? "" : ` ~ ${formatPercent(maxChance, 1)}`); return ( <> - Estimated success chance: {chance} {props.action.isStealth ? : <>} - {props.action.isKill ? : <>} + Estimated success chance: {chance} {action.isStealth ? : <>} + {action.isKill ? : <>} ); } diff --git a/src/Bladeburner/ui/TeamSizeButton.tsx b/src/Bladeburner/ui/TeamSizeButton.tsx index 17bdd22d2..75f822d59 100644 --- a/src/Bladeburner/ui/TeamSizeButton.tsx +++ b/src/Bladeburner/ui/TeamSizeButton.tsx @@ -1,22 +1,24 @@ +import type { Bladeburner } from "../Bladeburner"; +import type { BlackOperation, Operation } from "../Actions"; + import React, { useState } from "react"; -import { Operation } from "../Operation"; -import { Bladeburner } from "../Bladeburner"; import { TeamSizeModal } from "./TeamSizeModal"; import { formatNumberNoSuffix } from "../../ui/formatNumber"; import Button from "@mui/material/Button"; -interface IProps { - action: Operation; + +interface TeamSizeButtonProps { + action: Operation | BlackOperation; bladeburner: Bladeburner; } -export function TeamSizeButton(props: IProps): React.ReactElement { +export function TeamSizeButton({ action, bladeburner }: TeamSizeButtonProps): React.ReactElement { const [open, setOpen] = useState(false); return ( <> - - setOpen(false)} action={props.action} bladeburner={props.bladeburner} /> + setOpen(false)} action={action} bladeburner={bladeburner} /> ); } diff --git a/src/Bladeburner/ui/TeamSizeModal.tsx b/src/Bladeburner/ui/TeamSizeModal.tsx index ea4f56880..42f53a939 100644 --- a/src/Bladeburner/ui/TeamSizeModal.tsx +++ b/src/Bladeburner/ui/TeamSizeModal.tsx @@ -1,5 +1,5 @@ -import type { Action } from "../Action"; import type { Bladeburner } from "../Bladeburner"; +import type { BlackOperation, Operation } from "../Actions"; import React, { useState } from "react"; import { dialogBoxCreate } from "../../ui/React/DialogBox"; @@ -8,7 +8,7 @@ import { Button, TextField, Typography } from "@mui/material"; interface TeamSizeModalProps { bladeburner: Bladeburner; - action: Action; + action: Operation | BlackOperation; open: boolean; onClose: () => void; } diff --git a/src/Bladeburner/utils/loadActionIdentifier.ts b/src/Bladeburner/utils/loadActionIdentifier.ts new file mode 100644 index 000000000..4920d0881 --- /dev/null +++ b/src/Bladeburner/utils/loadActionIdentifier.ts @@ -0,0 +1,25 @@ +import type { ActionIdentifier } from "../Types"; +import { BladeActionType } from "@enums"; +import { assertLoadingType } from "../../utils/TypeAssertion"; +import { getEnumHelper } from "../../utils/EnumHelper"; + +/** Loads an action identifier + * This is used for loading ActionIdentifier class objects from pre-2.6.1 + * Should load both the old format and the new format */ +export function loadActionIdentifier(identifier: unknown): ActionIdentifier | null { + if (!identifier || typeof identifier !== "object") return null; + assertLoadingType(identifier); + if (getEnumHelper("BladeBlackOpName").isMember(identifier.name)) { + return { type: BladeActionType.blackOp, name: identifier.name }; + } + if (getEnumHelper("BladeContractName").isMember(identifier.name)) { + return { type: BladeActionType.contract, name: identifier.name }; + } + if (getEnumHelper("BladeOperationName").isMember(identifier.name)) { + return { type: BladeActionType.operation, name: identifier.name }; + } + if (getEnumHelper("BladeGeneralActionName").isMember(identifier.name)) { + return { type: BladeActionType.general, name: identifier.name }; + } + return null; +} diff --git a/src/Company/Companies.ts b/src/Company/Companies.ts index 99fbe9d00..93ba18479 100644 --- a/src/Company/Companies.ts +++ b/src/Company/Companies.ts @@ -1,7 +1,8 @@ // Constructs all CompanyPosition objects using the metadata in data/companypositions.ts import { getCompaniesMetadata } from "./data/CompaniesMetadata"; import { Company } from "./Company"; -import { Reviver, assertLoadingType } from "../utils/JSONReviver"; +import { Reviver } from "../utils/JSONReviver"; +import { assertLoadingType } from "../utils/TypeAssertion"; import { CompanyName } from "./Enums"; import { PartialRecord, createEnumKeyedRecord } from "../Types/Record"; import { getEnumHelper } from "../utils/EnumHelper"; diff --git a/src/Corporation/Enums.ts b/src/Corporation/Enums.ts index 8a8e7d977..fa852a6d5 100644 --- a/src/Corporation/Enums.ts +++ b/src/Corporation/Enums.ts @@ -1,4 +1,4 @@ -import { Member } from "src/types"; +import type { Member } from "../types"; export enum IndustryType { Water = "Water Utilities", diff --git a/src/DevMenu/ui/BladeburnerDev.tsx b/src/DevMenu/ui/BladeburnerDev.tsx index 94f6ea8de..7a6cde535 100644 --- a/src/DevMenu/ui/BladeburnerDev.tsx +++ b/src/DevMenu/ui/BladeburnerDev.tsx @@ -15,10 +15,10 @@ import Select, { SelectChangeEvent } from "@mui/material/Select"; import DeleteIcon from "@mui/icons-material/Delete"; import { Adjuster } from "./Adjuster"; import { Player } from "@player"; -import { CityName } from "@enums"; -import { Skills as AllSkills } from "../../Bladeburner/Skills"; -import { SkillNames } from "../../Bladeburner/data/SkillNames"; +import { BladeSkillName, CityName } from "@enums"; +import { Skills as AllSkills } from "../../Bladeburner/data/Skills"; import { Bladeburner } from "../../Bladeburner/Bladeburner"; +import { getEnumHelper } from "../../utils/EnumHelper"; const bigNumber = 1e27; @@ -57,24 +57,23 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R }; // Skill functions - const [skill, setSkill] = useState(SkillNames.BladesIntuition); + const [skillName, setSkillName] = useState(BladeSkillName.bladesIntuition); function setSkillDropdown(event: SelectChangeEvent): void { - setSkill(event.target.value); + if (!getEnumHelper("BladeSkillName").isMember(event.target.value)) return; + setSkillName(event.target.value); } const modifySkill = (modifier: number) => (levelchange: number) => { - if (bladeburner.skills[AllSkills[skill].name] == null) resetSkill(); if (!isNaN(levelchange)) { - bladeburner.skills[AllSkills[skill].name] += levelchange * modifier; + bladeburner.setSkillLevel(skillName, bladeburner.getSkillLevel(skillName) + levelchange * modifier); bladeburner.updateSkillMultipliers(); } }; const addTonsOfSkill = () => { - if (bladeburner.skills[AllSkills[skill].name] == null) resetSkill(); - bladeburner.skills[AllSkills[skill].name] += bigNumber; + bladeburner.setSkillLevel(skillName, bladeburner.getSkillLevel(skillName) + bigNumber); bladeburner.updateSkillMultipliers(); }; const resetSkill = () => { - bladeburner.skills[AllSkills[skill].name] = 0; + bladeburner.setSkillLevel(skillName, 0); bladeburner.updateSkillMultipliers(); }; @@ -82,6 +81,7 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R const AllContracts = bladeburner.contracts; const [contractTarget, setContract] = useState(AllContracts.Tracking.name); function setContractDropdown(event: SelectChangeEvent): void { + if (!getEnumHelper("BladeContractName").isMember(event.target.value)) return; setContract(event.target.value); } const modifyContractLevel = (modifier: number) => (levelchange: number) => { @@ -117,6 +117,7 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R const AllOperations = bladeburner.operations; const [operationTarget, setOperation] = useState(AllOperations.Investigation.name); function setOperationDropdown(event: SelectChangeEvent): void { + if (!getEnumHelper("BladeOperationName").isMember(event.target.value)) return; setOperation(event.target.value); } const modifyOperationLevel = (modifier: number) => (levelchange: number) => { @@ -232,7 +233,7 @@ export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): R - {Object.values(AllSkills).map((skill) => ( {skill.name} diff --git a/src/DevMenu/ui/General.tsx b/src/DevMenu/ui/General.tsx index a3e5b8101..c00ff189d 100644 --- a/src/DevMenu/ui/General.tsx +++ b/src/DevMenu/ui/General.tsx @@ -20,7 +20,6 @@ import { NumberInput } from "../../ui/React/NumberInput"; import { Hashes } from "../../ui/React/Hashes"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; -import { Bladeburner } from "../../Bladeburner/Bladeburner"; import { GangConstants } from "../../Gang/data/Constants"; import { checkForMessagesToSend } from "../../Message/MessageHelpers"; import { getEnumHelper } from "../../utils/EnumHelper"; @@ -72,7 +71,7 @@ export function General({ parentRerender }: { parentRerender: () => void }): Rea // Blade functions const joinBladeburner = () => { - Player.bladeburner = new Bladeburner(); + Player.startBladeburner(); parentRerender(); }; const leaveBladeburner = () => { diff --git a/src/Faction/Factions.ts b/src/Faction/Factions.ts index 8642e6150..d1f4cb3e7 100644 --- a/src/Faction/Factions.ts +++ b/src/Faction/Factions.ts @@ -3,7 +3,8 @@ import type { PlayerObject } from "../PersonObjects/Player/PlayerObject"; import { FactionName, FactionDiscovery } from "@enums"; import { Faction } from "./Faction"; -import { Reviver, assertLoadingType } from "../utils/JSONReviver"; +import { Reviver } from "../utils/JSONReviver"; +import { assertLoadingType } from "../utils/TypeAssertion"; import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record"; import { Augmentations } from "../Augmentation/Augmentations"; import { getEnumHelper } from "../utils/EnumHelper"; diff --git a/src/Go/SaveLoad.ts b/src/Go/SaveLoad.ts index 4ef2c6858..19b4f5365 100644 --- a/src/Go/SaveLoad.ts +++ b/src/Go/SaveLoad.ts @@ -5,7 +5,7 @@ import { Truthy } from "lodash"; import { GoColor, GoOpponent } from "@enums"; import { Go } from "./Go"; import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis"; -import { assertLoadingType } from "../utils/JSONReviver"; +import { assertLoadingType } from "../utils/TypeAssertion"; import { getEnumHelper } from "../utils/EnumHelper"; import { boardSizes } from "./Constants"; import { isInteger, isNumber } from "../types"; diff --git a/src/Netscript/NetscriptHelpers.tsx b/src/Netscript/NetscriptHelpers.tsx index a8c9bccf4..e4c1ef1f2 100644 --- a/src/Netscript/NetscriptHelpers.tsx +++ b/src/Netscript/NetscriptHelpers.tsx @@ -32,7 +32,15 @@ import { arrayToString } from "../utils/helpers/ArrayHelpers"; import { HacknetServer } from "../Hacknet/HacknetServer"; import { BaseServer } from "../Server/BaseServer"; import { RamCostConstants } from "./RamCostGenerator"; -import { isPositiveInteger, PositiveInteger, Unknownify, isPositiveNumber, PositiveNumber } from "../types"; +import { + isPositiveInteger, + PositiveInteger, + Unknownify, + isPositiveNumber, + PositiveNumber, + PositiveSafeInteger, + isPositiveSafeInteger, +} from "../types"; import { Engine } from "../engine"; import { resolveFilePath, FilePath } from "../Paths/FilePath"; import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath"; @@ -45,6 +53,7 @@ export const helpers = { string, number, positiveInteger, + positiveSafeInteger, scriptArgs, runOptions, spawnOptions, @@ -120,6 +129,16 @@ function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): Po } return n; } + +/** Convert provided value v for argument argName to a positive safe integer, throwing if it looks like something else. */ +function positiveSafeInteger(ctx: NetscriptContext, argName: string, v: unknown): PositiveSafeInteger { + const n = number(ctx, argName, v); + if (!isPositiveSafeInteger(n)) { + throw errorMessage(ctx, `${argName} should be a positive safe integer, was ${n}`, "TYPE"); + } + return n; +} + /** Convert provided value v for argument argName to a positive number, throwing if it looks like something else. */ function positiveNumber(ctx: NetscriptContext, argName: string, v: unknown): PositiveNumber { const n = number(ctx, argName, v); diff --git a/src/NetscriptFunctions/Bladeburner.ts b/src/NetscriptFunctions/Bladeburner.ts index e01d4ff45..307ba3bb7 100644 --- a/src/NetscriptFunctions/Bladeburner.ts +++ b/src/NetscriptFunctions/Bladeburner.ts @@ -1,13 +1,16 @@ import type { Bladeburner as INetscriptBladeburner } from "@nsdefs"; -import type { Action } from "../Bladeburner/Action"; +import type { Action, LevelableAction } from "../Bladeburner/Types"; import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper"; import { Player } from "@player"; +import { BladeActionType, BladeContractName, BladeGeneralActionName, BladeOperationName, BladeSkillName } from "@enums"; import { Bladeburner, BladeburnerPromise } from "../Bladeburner/Bladeburner"; import { currentNodeMults } from "../BitNode/BitNodeMultipliers"; -import { BlackOperation } from "../Bladeburner/BlackOperation"; import { helpers } from "../Netscript/NetscriptHelpers"; import { getEnumHelper } from "../utils/EnumHelper"; +import { Skills } from "../Bladeburner/data/Skills"; +import { assertString } from "../Netscript/TypeAssertion"; +import { BlackOperations, blackOpsArray } from "../Bladeburner/data/BlackOperations"; export function NetscriptBladeburner(): InternalAPI { const checkBladeburnerAccess = function (ctx: NetscriptContext): void { @@ -25,88 +28,88 @@ export function NetscriptBladeburner(): InternalAPI { return bladeburner; }; - const getBladeburnerActionObject = function (ctx: NetscriptContext, type: string, name: string): Action { + function getAction(ctx: NetscriptContext, type: unknown, name: unknown): Action { const bladeburner = Player.bladeburner; + assertString(ctx, "type", type); + assertString(ctx, "name", name); if (bladeburner === null) throw new Error("Must have joined bladeburner"); - const actionId = bladeburner.getActionIdFromTypeAndName(type, name); - if (!actionId) { - throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`); - } - const actionObj = bladeburner.getActionObject(actionId); - if (!actionObj) { - throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`); - } + const action = bladeburner.getActionFromTypeAndName(type, name); + if (!action) throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`); + return action; + } - return actionObj; - }; + function isLevelableAction(action: Action): action is LevelableAction { + return action.type === BladeActionType.contract || action.type === BladeActionType.operation; + } + + function getLevelableAction(ctx: NetscriptContext, type: unknown, name: unknown): LevelableAction { + const action = getAction(ctx, type, name); + if (!isLevelableAction(action)) { + throw helpers.errorMessage( + ctx, + `Actions of type ${action.type} are not levelable, ${ctx.functionPath} requires a levelable action`, + ); + } + return action; + } return { inBladeburner: () => () => !!Player.bladeburner, getContractNames: (ctx) => () => { - const bladeburner = getBladeburner(ctx); - return bladeburner.getContractNamesNetscriptFn(); + getBladeburner(ctx); + return Object.values(BladeContractName); }, getOperationNames: (ctx) => () => { - const bladeburner = getBladeburner(ctx); - return bladeburner.getOperationNamesNetscriptFn(); + getBladeburner(ctx); + return Object.values(BladeOperationName); }, getBlackOpNames: (ctx) => () => { - const bladeburner = getBladeburner(ctx); - return bladeburner.getBlackOpNamesNetscriptFn(); + getBladeburner(ctx); + // Ensures they are sent in the correct order + return blackOpsArray.map((blackOp) => blackOp.name); }, getNextBlackOp: (ctx) => () => { const bladeburner = getBladeburner(ctx); - return bladeburner.getNextBlackOp(); + if (bladeburner.numBlackOpsComplete >= blackOpsArray.length) return null; + const blackOp = blackOpsArray[bladeburner.numBlackOpsComplete]; + return { name: blackOp.name, rank: blackOp.reqdRank }; }, getBlackOpRank: (ctx) => (_blackOpName) => { - const blackOpName = helpers.string(ctx, "blackOpName", _blackOpName); checkBladeburnerAccess(ctx); - const action = getBladeburnerActionObject(ctx, "blackops", blackOpName); - if (!(action instanceof BlackOperation)) throw new Error("action was not a black operation"); - return action.reqdRank; + const blackOpName = getEnumHelper("BladeBlackOpName").nsGetMember(ctx, _blackOpName); + return BlackOperations[blackOpName].reqdRank; }, getGeneralActionNames: (ctx) => () => { - const bladeburner = getBladeburner(ctx); - return bladeburner.getGeneralActionNamesNetscriptFn(); + getBladeburner(ctx); + return Object.values(BladeGeneralActionName); }, getSkillNames: (ctx) => () => { - const bladeburner = getBladeburner(ctx); - return bladeburner.getSkillNamesNetscriptFn(); + getBladeburner(ctx); + return Object.values(BladeSkillName); }, - startAction: (ctx) => (_type, _name) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); + startAction: (ctx) => (type, name) => { const bladeburner = getBladeburner(ctx); - try { - return bladeburner.startActionNetscriptFn(type, name, ctx.workerScript); - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); - } + const action = getAction(ctx, type, name); + const attempt = bladeburner.startAction(action.id); + helpers.log(ctx, () => attempt.message); + return !!attempt.success; }, stopBladeburnerAction: (ctx) => () => { const bladeburner = getBladeburner(ctx); + helpers.log(ctx, () => `Stopping current Bladeburner action.`); return bladeburner.resetAction(); }, getCurrentAction: (ctx) => () => { const bladeburner = getBladeburner(ctx); - return bladeburner.getTypeAndNameFromActionId(bladeburner.action); + // Temporary bad return type to not be an API break (idle should just return null) + if (!bladeburner.action) return { type: "Idle", name: "Idle" }; + return { ...bladeburner.action }; }, - getActionTime: (ctx) => (_type, _name) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); + getActionTime: (ctx) => (type, name) => { const bladeburner = getBladeburner(ctx); - try { - const time = bladeburner.getActionTimeNetscriptFn(Player, type, name); - if (typeof time === "string") { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - helpers.log(ctx, () => errorLogText); - return -1; - } else { - return time; - } - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); - } + const action = getAction(ctx, type, name); + // return ms instead of seconds + return action.getActionTime(bladeburner, Player) * 1000; }, getActionCurrentTime: (ctx) => () => { const bladeburner = getBladeburner(ctx); @@ -119,93 +122,70 @@ export function NetscriptBladeburner(): InternalAPI { throw helpers.errorMessage(ctx, String(e)); } }, - getActionEstimatedSuccessChance: (ctx) => (_type, _name) => { + getActionEstimatedSuccessChance: (ctx) => (type, name) => { const bladeburner = getBladeburner(ctx); - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); - try { - const chance = bladeburner.getActionEstimatedSuccessChanceNetscriptFn(Player, type, name); - if (typeof chance === "string") { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; - helpers.log(ctx, () => errorLogText); - return [-1, -1]; - } else { - return chance; - } - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); - } + const action = getAction(ctx, type, name); + return action.getSuccessRange(bladeburner, Player); }, - getActionRepGain: (ctx) => (_type, _name, _level) => { + getActionRepGain: (ctx) => (type, name, _level) => { checkBladeburnerAccess(ctx); - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); - const action = getBladeburnerActionObject(ctx, type, name); - const level = _level === undefined ? action.level : helpers.number(ctx, "level", _level); - const rewardMultiplier = Math.pow(action.rewardFac, level - 1); + const action = getAction(ctx, type, name); + const level = isLevelableAction(action) ? helpers.number(ctx, "level", _level ?? action.level) : 1; + const rewardMultiplier = isLevelableAction(action) ? Math.pow(action.rewardFac, level - 1) : 1; return action.rankGain * rewardMultiplier * currentNodeMults.BladeburnerRank; }, - getActionCountRemaining: (ctx) => (_type, _name) => { + getActionCountRemaining: (ctx) => (type, name) => { const bladeburner = getBladeburner(ctx); - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); - try { - return bladeburner.getActionCountRemainingNetscriptFn(type, name, ctx.workerScript); - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); + const action = getAction(ctx, type, name); + switch (action.type) { + case BladeActionType.general: + return Infinity; + case BladeActionType.blackOp: + return bladeburner.numBlackOpsComplete > action.n ? 0 : 1; + case BladeActionType.contract: + case BladeActionType.operation: + return action.count; } }, - getActionMaxLevel: (ctx) => (_type, _name) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); + getActionMaxLevel: (ctx) => (type, name) => { checkBladeburnerAccess(ctx); - const action = getBladeburnerActionObject(ctx, type, name); + const action = getLevelableAction(ctx, type, name); return action.maxLevel; }, - getActionCurrentLevel: (ctx) => (_type, _name) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); + getActionCurrentLevel: (ctx) => (type, name) => { checkBladeburnerAccess(ctx); - const action = getBladeburnerActionObject(ctx, type, name); + const action = getLevelableAction(ctx, type, name); return action.level; }, - getActionAutolevel: (ctx) => (_type, _name) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); + getActionAutolevel: (ctx) => (type, name) => { checkBladeburnerAccess(ctx); - const action = getBladeburnerActionObject(ctx, type, name); + const action = getLevelableAction(ctx, type, name); return action.autoLevel; }, - getActionSuccesses: (ctx) => (_type, _name) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); + getActionSuccesses: (ctx) => (type, name) => { checkBladeburnerAccess(ctx); - const action = getBladeburnerActionObject(ctx, type, name); + const action = getLevelableAction(ctx, type, name); return action.successes; }, setActionAutolevel: (ctx) => - (_type, _name, _autoLevel = true) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); + (type, name, _autoLevel = true) => { const autoLevel = !!_autoLevel; checkBladeburnerAccess(ctx); - const action = getBladeburnerActionObject(ctx, type, name); + const action = getLevelableAction(ctx, type, name); action.autoLevel = autoLevel; + helpers.log(ctx, () => `Autolevel for ${action.name} has been ${autoLevel ? "enabled" : "disabled"}`); }, - setActionLevel: - (ctx) => - (_type, _name, _level = 1) => { - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); - const level = helpers.number(ctx, "level", _level); - checkBladeburnerAccess(ctx); - const action = getBladeburnerActionObject(ctx, type, name); - if (level < 1 || level > action.maxLevel) { - throw helpers.errorMessage(ctx, `Level must be between 1 and ${action.maxLevel}, is ${level}`); - } - action.level = level; - }, + setActionLevel: (ctx) => (type, name, _level) => { + const level = helpers.positiveInteger(ctx, "level", _level ?? 1); + checkBladeburnerAccess(ctx); + const action = getLevelableAction(ctx, type, name); + if (level < 1 || level > action.maxLevel) { + throw helpers.errorMessage(ctx, `Level must be between 1 and ${action.maxLevel}, is ${level}`); + } + action.level = level; + helpers.log(ctx, () => `Set level for ${action.name} to ${level}`); + }, getRank: (ctx) => () => { const bladeburner = getBladeburner(ctx); return bladeburner.rank; @@ -215,57 +195,57 @@ export function NetscriptBladeburner(): InternalAPI { return bladeburner.skillPoints; }, getSkillLevel: (ctx) => (_skillName) => { - const skillName = helpers.string(ctx, "skillName", _skillName); const bladeburner = getBladeburner(ctx); - try { - return bladeburner.getSkillLevelNetscriptFn(skillName, ctx.workerScript); - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); + const skillName = getEnumHelper("BladeSkillName").nsGetMember(ctx, _skillName, "skillName"); + return bladeburner.getSkillLevel(skillName); + }, + getSkillUpgradeCost: (ctx) => (_skillName, _count) => { + const bladeburner = getBladeburner(ctx); + const skillName = getEnumHelper("BladeSkillName").nsGetMember(ctx, _skillName, "skillName"); + const count = helpers.positiveSafeInteger(ctx, "count", _count ?? 1); + const currentLevel = bladeburner.getSkillLevel(skillName); + return Skills[skillName].calculateCost(currentLevel, count); + }, + upgradeSkill: (ctx) => (_skillName, _count) => { + const bladeburner = getBladeburner(ctx); + const skillName = getEnumHelper("BladeSkillName").nsGetMember(ctx, _skillName, "skillName"); + const count = helpers.positiveSafeInteger(ctx, "count", _count ?? 1); + const attempt = bladeburner.upgradeSkill(skillName, count); + helpers.log(ctx, () => attempt.message); + return !!attempt.success; + }, + getTeamSize: (ctx) => (type, name) => { + const bladeburner = getBladeburner(ctx); + if (!type && !name) return bladeburner.teamSize; + const action = getAction(ctx, type, name); + switch (action.type) { + case BladeActionType.general: + case BladeActionType.contract: + return 0; + case BladeActionType.blackOp: + case BladeActionType.operation: + return action.teamCount; } }, - getSkillUpgradeCost: - (ctx) => - (_skillName, _count = 1) => { - const bladeburner = getBladeburner(ctx); - const skillName = helpers.string(ctx, "skillName", _skillName); - const count = helpers.number(ctx, "count", _count); - try { - return bladeburner.getSkillUpgradeCostNetscriptFn(skillName, count, ctx.workerScript); - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); - } - }, - upgradeSkill: - (ctx) => - (_skillName, _count = 1) => { - const bladeburner = getBladeburner(ctx); - const skillName = helpers.string(ctx, "skillName", _skillName); - const count = helpers.number(ctx, "count", _count); - try { - return bladeburner.upgradeSkillNetscriptFn(skillName, count, ctx.workerScript); - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); - } - }, - getTeamSize: (ctx) => (_type, _name) => { + setTeamSize: (ctx) => (type, name, _size) => { const bladeburner = getBladeburner(ctx); - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); - try { - return bladeburner.getTeamSizeNetscriptFn(type, name, ctx.workerScript); - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); + const action = getAction(ctx, type, name); + const size = helpers.positiveInteger(ctx, "size", _size); + if (size > bladeburner.teamSize) { + helpers.log(ctx, () => `Failed to set team size due to not enough team members.`); + return -1; } - }, - setTeamSize: (ctx) => (_type, _name, _size) => { - const bladeburner = getBladeburner(ctx); - const type = helpers.string(ctx, "type", _type); - const name = helpers.string(ctx, "name", _name); - const size = helpers.number(ctx, "size", _size); - try { - return bladeburner.setTeamSizeNetscriptFn(type, name, size, ctx.workerScript); - } catch (e: unknown) { - throw helpers.errorMessage(ctx, String(e)); + switch (action.type) { + case BladeActionType.contract: + case BladeActionType.general: + helpers.log(ctx, () => "Only valid for Operations and Black Operations"); + return -1; + case BladeActionType.blackOp: + case BladeActionType.operation: { + action.teamCount = size; + helpers.log(ctx, () => `Set team size for ${action.name} to ${size}`); + return size; + } } }, getCityEstimatedPopulation: (ctx) => (_cityName) => { @@ -299,7 +279,9 @@ export function NetscriptBladeburner(): InternalAPI { }, joinBladeburnerFaction: (ctx) => () => { const bladeburner = getBladeburner(ctx); - return bladeburner.joinBladeburnerFactionNetscriptFn(ctx.workerScript); + const attempt = bladeburner.joinFaction(); + helpers.log(ctx, () => attempt.message); + return !!attempt.success; }, joinBladeburnerDivision: (ctx) => () => { if (Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0) { @@ -314,7 +296,7 @@ export function NetscriptBladeburner(): InternalAPI { Player.skills.dexterity >= 100 && Player.skills.agility >= 100 ) { - Player.bladeburner = new Bladeburner(); + Player.startBladeburner(); helpers.log(ctx, () => "You have been accepted into the Bladeburner division"); return true; diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index efd57bb70..afbf1d020 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -3,7 +3,6 @@ import type { Singularity as ISingularity, Task as ITask } from "@nsdefs"; import { Player } from "@player"; import { AugmentationName, - BlackOperationName, CityName, FactionName, FactionWorkType, @@ -57,6 +56,7 @@ import { root } from "../Paths/Directory"; import { getRecordEntries } from "../Types/Record"; import { JobTracks } from "../Company/data/JobTracks"; import { ServerConstants } from "../Server/data/Constants"; +import { blackOpsArray } from "../Bladeburner/data/BlackOperations"; export function NetscriptSingularity(): InternalAPI { const runAfterReset = function (cbScript: ScriptFilePath) { @@ -1122,7 +1122,7 @@ export function NetscriptSingularity(): InternalAPI { }; const bladeburnerRequirements = () => { if (!Player.bladeburner) return false; - return Player.bladeburner.blackops[BlackOperationName.OperationDaedalus]; + return Player.bladeburner.numBlackOpsComplete >= blackOpsArray.length; }; if (!hackingRequirements() && !bladeburnerRequirements()) { diff --git a/src/NetscriptFunctions/Sleeve.ts b/src/NetscriptFunctions/Sleeve.ts index e622aafd6..49260f4d7 100644 --- a/src/NetscriptFunctions/Sleeve.ts +++ b/src/NetscriptFunctions/Sleeve.ts @@ -1,17 +1,20 @@ import type { Augmentation } from "../Augmentation/Augmentation"; import type { Sleeve as NetscriptSleeve } from "@nsdefs"; +import type { ActionIdentifier } from "../Bladeburner/Types"; import { Player } from "@player"; +import { BladeActionType } from "@enums"; import { Augmentations } from "../Augmentation/Augmentations"; import { findCrime } from "../Crime/CrimeHelpers"; import { getEnumHelper } from "../utils/EnumHelper"; import { InternalAPI, NetscriptContext, setRemovedFunctions } from "../Netscript/APIWrapper"; -import { isSleeveBladeburnerWork } from "../PersonObjects/Sleeve/Work/SleeveBladeburnerWork"; +import { SleeveBladeburnerWork } from "../PersonObjects/Sleeve/Work/SleeveBladeburnerWork"; import { isSleeveFactionWork } from "../PersonObjects/Sleeve/Work/SleeveFactionWork"; import { isSleeveCompanyWork } from "../PersonObjects/Sleeve/Work/SleeveCompanyWork"; import { helpers } from "../Netscript/NetscriptHelpers"; import { getAugCost } from "../Augmentation/AugmentationHelpers"; import { Factions } from "../Faction/Factions"; +import { SleeveWorkType } from "../PersonObjects/Sleeve/Work/Work"; export function NetscriptSleeve(): InternalAPI { const checkSleeveAPIAccess = function (ctx: NetscriptContext) { @@ -233,32 +236,25 @@ export function NetscriptSleeve(): InternalAPI { setToBladeburnerAction: (ctx) => (_sleeveNumber, _action, _contract?) => { const sleeveNumber = helpers.number(ctx, "sleeveNumber", _sleeveNumber); const action = helpers.string(ctx, "action", _action); - let contract: string; - if (typeof _contract === "undefined") { - contract = "------"; - } else { - contract = helpers.string(ctx, "contract", _contract); - } checkSleeveAPIAccess(ctx); checkSleeveNumber(ctx, sleeveNumber); - - // Cannot Take on Contracts if another sleeve is performing that action if (action === "Take on contracts") { + const contract = getEnumHelper("BladeContractName").nsGetMember(ctx, _contract); for (let i = 0; i < Player.sleeves.length; ++i) { - if (i === sleeveNumber) { - continue; - } - const other = Player.sleeves[i]; - if (isSleeveBladeburnerWork(other.currentWork) && other.currentWork.actionName === contract) { + if (i === sleeveNumber) continue; + const otherWork = Player.sleeves[i].currentWork; + if (otherWork?.type === SleeveWorkType.BLADEBURNER && otherWork.actionId.name === contract) { throw helpers.errorMessage( ctx, `Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`, ); } } + const actionId: ActionIdentifier = { type: BladeActionType.contract, name: contract }; + Player.sleeves[sleeveNumber].startWork(new SleeveBladeburnerWork({ actionId })); } - return Player.sleeves[sleeveNumber].bladeburner(action, contract); + return Player.sleeves[sleeveNumber].bladeburner(action); }, }; diff --git a/src/PersonObjects/Person.ts b/src/PersonObjects/Person.ts index 013848120..1e2ab2e42 100644 --- a/src/PersonObjects/Person.ts +++ b/src/PersonObjects/Person.ts @@ -49,7 +49,6 @@ export abstract class Person implements IPerson { gainIntelligenceExp = personMethods.gainIntelligenceExp; gainStats = personMethods.gainStats; regenerateHp = personMethods.regenerateHp; - queryStatFromString = personMethods.queryStatFromString; updateSkillLevels = personMethods.updateSkillLevels; hasAugmentation = personMethods.hasAugmentation; calculateSkill = calculateSkill; //Class version is equal to imported version diff --git a/src/PersonObjects/PersonMethods.ts b/src/PersonObjects/PersonMethods.ts index 8a370df4e..f47e6874c 100644 --- a/src/PersonObjects/PersonMethods.ts +++ b/src/PersonObjects/PersonMethods.ts @@ -114,33 +114,6 @@ export function gainStats(this: Person, retValue: WorkStats): void { this.gainIntelligenceExp(retValue.intExp); } -//Given a string expression like "str" or "strength", returns the given stat -export function queryStatFromString(this: Person, str: string): number { - const tempStr = str.toLowerCase(); - if (tempStr.includes("hack")) { - return this.skills.hacking; - } - if (tempStr.includes("str")) { - return this.skills.strength; - } - if (tempStr.includes("def")) { - return this.skills.defense; - } - if (tempStr.includes("dex")) { - return this.skills.dexterity; - } - if (tempStr.includes("agi")) { - return this.skills.agility; - } - if (tempStr.includes("cha")) { - return this.skills.charisma; - } - if (tempStr.includes("int")) { - return this.skills.intelligence; - } - return 0; -} - export function regenerateHp(this: Person, amt: number): void { if (typeof amt !== "number") { console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`); diff --git a/src/PersonObjects/Player/PlayerObjectBladeburnerMethods.ts b/src/PersonObjects/Player/PlayerObjectBladeburnerMethods.ts index 1dd8c4816..b94418474 100644 --- a/src/PersonObjects/Player/PlayerObjectBladeburnerMethods.ts +++ b/src/PersonObjects/Player/PlayerObjectBladeburnerMethods.ts @@ -8,4 +8,5 @@ export function canAccessBladeburner(this: PlayerObject): boolean { export function startBladeburner(this: PlayerObject): void { this.bladeburner = new Bladeburner(); + this.bladeburner.init(); } diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index 1649eaeb9..cbbbdf35a 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -14,7 +14,6 @@ import type { SleeveWork } from "./Work/Work"; import { Player } from "@player"; import { Person } from "../Person"; -import { Contracts } from "../../Bladeburner/data/Contracts"; import { CONSTANTS } from "../../Constants"; import { ClassType, @@ -26,12 +25,13 @@ import { UniversityClassType, CompanyName, FactionName, + BladeActionType, + BladeGeneralActionName, } from "@enums"; import { Factions } from "../../Faction/Factions"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../utils/JSONReviver"; -import { formatPercent } from "../../ui/formatNumber"; import { SleeveClassWork } from "./Work/SleeveClassWork"; import { SleeveSynchroWork } from "./Work/SleeveSynchroWork"; import { SleeveRecoveryWork } from "./Work/SleeveRecoveryWork"; @@ -391,24 +391,44 @@ export class Sleeve extends Person implements SleevePerson { } /** Begin a bladeburner task */ - bladeburner(action: string, contract: string): boolean { + bladeburner(action: string, contract?: string): boolean { if (!Player.bladeburner) return false; switch (action) { case "Training": - this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Training" })); + this.startWork( + new SleeveBladeburnerWork({ + actionId: { type: BladeActionType.general, name: BladeGeneralActionName.training }, + }), + ); return true; case "Field analysis": case "Field Analysis": - this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Field Analysis" })); + this.startWork( + new SleeveBladeburnerWork({ + actionId: { type: BladeActionType.general, name: BladeGeneralActionName.fieldAnalysis }, + }), + ); return true; case "Recruitment": - this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Recruitment" })); + this.startWork( + new SleeveBladeburnerWork({ + actionId: { type: BladeActionType.general, name: BladeGeneralActionName.recruitment }, + }), + ); return true; case "Diplomacy": - this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Diplomacy" })); + this.startWork( + new SleeveBladeburnerWork({ + actionId: { type: BladeActionType.general, name: BladeGeneralActionName.diplomacy }, + }), + ); return true; case "Hyperbolic Regeneration Chamber": - this.startWork(new SleeveBladeburnerWork({ type: "General", name: "Hyperbolic Regeneration Chamber" })); + this.startWork( + new SleeveBladeburnerWork({ + actionId: { type: BladeActionType.general, name: BladeGeneralActionName.hyperbolicRegen }, + }), + ); return true; case "Infiltrate synthoids": case "Infiltrate Synthoids": @@ -418,36 +438,13 @@ export class Sleeve extends Person implements SleevePerson { this.startWork(new SleeveSupportWork()); return true; case "Take on contracts": - if (!Contracts[contract]) return false; - this.startWork(new SleeveBladeburnerWork({ type: "Contracts", name: contract })); + if (!getEnumHelper("BladeContractName").isMember(contract)) return false; + this.startWork(new SleeveBladeburnerWork({ actionId: { type: BladeActionType.contract, name: contract } })); return true; } return false; } - recruitmentSuccessChance(): number { - return Math.max(0, Math.min(1, Player.bladeburner?.getRecruitmentSuccessChance(this) ?? 0)); - } - - contractSuccessChance(type: string, name: string): string { - const bb = Player.bladeburner; - if (bb === null) { - const errorLogText = `bladeburner is null`; - console.error(`Function: sleeves.contractSuccessChance; Message: '${errorLogText}'`); - return "0%"; - } - const chances = bb.getActionEstimatedSuccessChanceNetscriptFn(this, type, name); - if (typeof chances === "string") { - console.error(`Function: sleeves.contractSuccessChance; Message: '${chances}'`); - return "0%"; - } - if (chances[0] >= 1) { - return "100%"; - } else { - return `${formatPercent(chances[0])} - ${formatPercent(chances[1])}`; - } - } - takeDamage(amt: number): boolean { if (typeof amt !== "number") { console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`); diff --git a/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts index 0f56925a2..308a0cf6b 100644 --- a/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts +++ b/src/PersonObjects/Sleeve/Work/SleeveBladeburnerWork.ts @@ -1,39 +1,40 @@ +import type { Sleeve } from "../Sleeve"; +import type { ActionIdentifier } from "../../../Bladeburner/Types"; import type { PromisePair } from "../../../Types/Promises"; import { Player } from "@player"; +import { BladeActionType, BladeGeneralActionName } from "@enums"; import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../../../utils/JSONReviver"; -import { Sleeve } from "../Sleeve"; import { applySleeveGains, SleeveWorkClass, SleeveWorkType } from "./Work"; import { CONSTANTS } from "../../../Constants"; -import { GeneralActions } from "../../../Bladeburner/data/GeneralActions"; import { scaleWorkStats } from "../../../Work/WorkStats"; import { getKeyList } from "../../../utils/helpers/getKeyList"; +import { loadActionIdentifier } from "../../../Bladeburner/utils/loadActionIdentifier"; +import { invalidWork } from "../../../Work/InvalidWork"; interface SleeveBladeburnerWorkParams { - type: "General" | "Contracts"; - name: string; + actionId: ActionIdentifier & { type: BladeActionType.general | BladeActionType.contract }; } export const isSleeveBladeburnerWork = (w: SleeveWorkClass | null): w is SleeveBladeburnerWork => - w !== null && w.type === SleeveWorkType.BLADEBURNER; + w?.type === SleeveWorkType.BLADEBURNER; export class SleeveBladeburnerWork extends SleeveWorkClass { type: SleeveWorkType.BLADEBURNER = SleeveWorkType.BLADEBURNER; tasksCompleted = 0; cyclesWorked = 0; - actionType: "General" | "Contracts"; - actionName: string; + actionId: ActionIdentifier & { type: BladeActionType.general | BladeActionType.contract }; nextCompletionPair: PromisePair = { promise: null, resolve: null }; constructor(params?: SleeveBladeburnerWorkParams) { super(); - this.actionType = params?.type ?? "General"; - this.actionName = params?.name ?? "Field Analysis"; + this.actionId = params?.actionId ?? { type: BladeActionType.general, name: BladeGeneralActionName.fieldAnalysis }; } cyclesNeeded(sleeve: Sleeve): number { - const ret = Player.bladeburner?.getActionTimeNetscriptFn(sleeve, this.actionType, this.actionName); - if (!ret || typeof ret === "string") throw new Error(`Error querying ${this.actionName} time`); - return ret / CONSTANTS.MilliPerCycle; + if (!Player.bladeburner) return Infinity; + const action = Player.bladeburner.getActionObject(this.actionId); + const timeInMs = action.getActionTime(Player.bladeburner, sleeve) * 1000; + return timeInMs / CONSTANTS.MilliPerCycle; } finish() { @@ -47,30 +48,19 @@ export class SleeveBladeburnerWork extends SleeveWorkClass { process(sleeve: Sleeve, cycles: number) { if (!Player.bladeburner) return sleeve.stopWork(); this.cyclesWorked += cycles; - const actionIdent = Player.bladeburner.getActionIdFromTypeAndName(this.actionType, this.actionName); - if (!actionIdent) throw new Error(`Error getting ${this.actionName} action`); - if (this.actionType === "Contracts") { - const action = Player.bladeburner.getActionObject(actionIdent); - if (!action) throw new Error(`Error getting ${this.actionName} action object`); + if (this.actionId.type === BladeActionType.contract) { + const action = Player.bladeburner.getActionObject(this.actionId); if (action.count < 1) return sleeve.stopWork(); } while (this.cyclesWorked >= this.cyclesNeeded(sleeve)) { - if (this.actionType === "Contracts") { - const action = Player.bladeburner.getActionObject(actionIdent); - if (!action) throw new Error(`Error getting ${this.actionName} action object`); + if (this.actionId.type === BladeActionType.contract) { + const action = Player.bladeburner.getActionObject(this.actionId); if (action.count < 1) return sleeve.stopWork(); } - const retValue = Player.bladeburner.completeAction(sleeve, actionIdent, false); - if (this.actionType === "General") { - const exp = GeneralActions[this.actionName]?.exp; - if (!exp) throw new Error(`Somehow there was no exp for action ${this.actionType} ${this.actionName}`); - applySleeveGains(sleeve, scaleWorkStats(exp, sleeve.shockBonus(), false)); - } + const retValue = Player.bladeburner.completeAction(sleeve, this.actionId, false); + applySleeveGains(sleeve, scaleWorkStats(retValue, sleeve.shockBonus(), false)); - if (this.actionType === "Contracts") { - applySleeveGains(sleeve, scaleWorkStats(retValue, sleeve.shockBonus(), false)); - } this.tasksCompleted++; this.cyclesWorked -= this.cyclesNeeded(sleeve); // Resolve and reset nextCompletion promise @@ -86,8 +76,8 @@ export class SleeveBladeburnerWork extends SleeveWorkClass { APICopy(sleeve: Sleeve) { return { type: SleeveWorkType.BLADEBURNER as const, - actionType: this.actionType, - actionName: this.actionName, + actionType: this.actionId.type, + actionName: this.actionId.name, tasksCompleted: this.tasksCompleted, cyclesWorked: this.cyclesWorked, cyclesNeeded: this.cyclesNeeded(sleeve), @@ -104,6 +94,9 @@ export class SleeveBladeburnerWork extends SleeveWorkClass { /** Initializes a BladeburnerWork object from a JSON save state. */ static fromJSON(value: IReviverValue): SleeveBladeburnerWork { + const actionId = loadActionIdentifier(value.data?.actionId); + if (!actionId) return invalidWork(); + value.data.actionId = actionId; return Generic_fromJSON(SleeveBladeburnerWork, value.data, SleeveBladeburnerWork.savedKeys); } } diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx index 9c1595c3c..cad99afb6 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -31,7 +31,7 @@ function getWorkDescription(sleeve: Sleeve, progress: number): string { return "This sleeve is currently set to synchronize with the original consciousness. This causes the Sleeve's synchronization to increase."; case SleeveWorkType.BLADEBURNER: return ( - `This sleeve is currently attempting to perform ${work.actionName}.\n\nTasks Completed: ${formatInt( + `This sleeve is currently attempting to perform ${work.actionId.name}.\n\nTasks Completed: ${formatInt( work.tasksCompleted, )}\n \n` + `Progress: ${formatPercent(progress)}` ); diff --git a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx index 679d52ce0..8fe58bfaf 100644 --- a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx +++ b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx @@ -1,14 +1,20 @@ +import type { Sleeve } from "../Sleeve"; + import React, { useState } from "react"; -import { Sleeve } from "../Sleeve"; +import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; + import { Player } from "@player"; +import { + BladeActionType, + BladeContractName, + CityName, + FactionName, + FactionWorkType, + GymType, + LocationName, +} from "@enums"; import { Crimes } from "../../../Crime/Crimes"; -import { CityName, FactionName, FactionWorkType, GymType, LocationName } from "@enums"; import { Factions } from "../../../Faction/Factions"; -import Select, { SelectChangeEvent } from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import { isSleeveFactionWork } from "../Work/SleeveFactionWork"; -import { isSleeveCompanyWork } from "../Work/SleeveCompanyWork"; -import { isSleeveBladeburnerWork } from "../Work/SleeveBladeburnerWork"; import { getEnumHelper } from "../../../utils/EnumHelper"; import { SleeveWorkType } from "../Work/Work"; @@ -51,7 +57,7 @@ function possibleJobs(sleeve: Sleeve): string[] { if (sleeve === otherSleeve) { continue; } - if (isSleeveCompanyWork(otherSleeve.currentWork)) { + if (otherSleeve.currentWork?.type === SleeveWorkType.COMPANY) { forbiddenCompanies.push(otherSleeve.currentWork.companyName); } } @@ -70,7 +76,7 @@ function possibleFactions(sleeve: Sleeve): string[] { if (sleeve === otherSleeve) { continue; } - if (isSleeveFactionWork(otherSleeve.currentWork)) { + if (otherSleeve.currentWork?.type === SleeveWorkType.FACTION) { forbiddenFactions.push(otherSleeve.currentWork.factionName); } } @@ -90,24 +96,24 @@ function possibleFactions(sleeve: Sleeve): string[] { }); } -function possibleContracts(sleeve: Sleeve): string[] { +function possibleContracts(sleeve: Sleeve): BladeContractName[] | ["------"] { const bb = Player.bladeburner; if (bb === null) { return ["------"]; } - let contracts = bb.getContractNamesNetscriptFn(); + let contracts = Object.values(BladeContractName); for (const otherSleeve of Player.sleeves) { if (sleeve === otherSleeve) { continue; } - if (isSleeveBladeburnerWork(otherSleeve.currentWork) && otherSleeve.currentWork.actionType === "Contracts") { + if ( + otherSleeve.currentWork?.type === SleeveWorkType.BLADEBURNER && + otherSleeve.currentWork.actionId.type === BladeActionType.contract + ) { const w = otherSleeve.currentWork; - contracts = contracts.filter((x) => x != w.actionName); + contracts = contracts.filter((x) => x != w.actionId.name); } } - if (contracts.length === 0) { - return ["------"]; - } return contracts; } @@ -256,10 +262,10 @@ function getABC(sleeve: Sleeve): [string, string, string] { return ["Work for Faction", work.factionName, workNames[work.factionWorkType] ?? ""]; } case SleeveWorkType.BLADEBURNER: - if (work.actionType === "Contracts") { - return ["Perform Bladeburner Actions", "Take on contracts", work.actionName]; + if (work.actionId.type === BladeActionType.contract) { + return ["Perform Bladeburner Actions", "Take on contracts", work.actionId.name]; } - return ["Perform Bladeburner Actions", work.actionName, "------"]; + return ["Perform Bladeburner Actions", work.actionId.name, "------"]; case SleeveWorkType.CLASS: { if (!work.isGym()) return ["Take University Course", work.classType, work.location]; const gymNames: Record = { diff --git a/src/Prestige.ts b/src/Prestige.ts index fbe9510c6..96479e4a8 100755 --- a/src/Prestige.ts +++ b/src/Prestige.ts @@ -131,7 +131,7 @@ export function prestigeAugmentation(): void { // Cancel Bladeburner action if (Player.bladeburner) { - Player.bladeburner.prestige(); + Player.bladeburner.prestigeAugmentation(); } // BitNode 8: Ghost of Wall Street diff --git a/src/RemoteFileAPI/MessageDefinitions.ts b/src/RemoteFileAPI/MessageDefinitions.ts index a2c333165..abb7f5688 100644 --- a/src/RemoteFileAPI/MessageDefinitions.ts +++ b/src/RemoteFileAPI/MessageDefinitions.ts @@ -1,4 +1,4 @@ -import type { BaseServer } from "src/Server/BaseServer"; +import type { BaseServer } from "../Server/BaseServer"; export class RFAMessage { jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools? diff --git a/src/ui/React/CharacterOverview.tsx b/src/ui/React/CharacterOverview.tsx index abe57b70f..6b8de5da2 100644 --- a/src/ui/React/CharacterOverview.tsx +++ b/src/ui/React/CharacterOverview.tsx @@ -1,33 +1,26 @@ +import type { ActionIdentifier } from "../../Bladeburner/Types"; + // Root React Component for the Corporation UI import React, { useMemo, useState, useEffect, ReactNode } from "react"; - +import { Box, Button, IconButton, Table, TableBody, TableCell, TableRow, Tooltip, Typography } from "@mui/material"; +import SaveIcon from "@mui/icons-material/Save"; +import ClearAllIcon from "@mui/icons-material/ClearAll"; import { Theme, useTheme } from "@mui/material/styles"; import makeStyles from "@mui/styles/makeStyles"; import createStyles from "@mui/styles/createStyles"; + +import { Player } from "@player"; import { formatHp, formatMoney, formatSkill } from "../formatNumber"; import { Reputation } from "./Reputation"; import { KillScriptsModal } from "./KillScriptsModal"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableRow from "@mui/material/TableRow"; -import Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; -import IconButton from "@mui/material/IconButton"; -import SaveIcon from "@mui/icons-material/Save"; -import ClearAllIcon from "@mui/icons-material/ClearAll"; - import { Settings } from "../../Settings/Settings"; import { Router } from "../GameRoot"; import { Page } from "../Router"; -import { Player } from "@player"; import { StatsProgressOverviewCell } from "./StatsProgressBar"; import { currentNodeMults } from "../../BitNode/BitNodeMultipliers"; -import { Box, Tooltip } from "@mui/material"; - import { isClassWork } from "../../Work/ClassWork"; import { CONSTANTS } from "../../Constants"; import { isCreateProgramWork } from "../../Work/CreateProgramWork"; @@ -36,7 +29,6 @@ import { isFactionWork } from "../../Work/FactionWork"; import { ReputationRate } from "./ReputationRate"; import { isCompanyWork } from "../../Work/CompanyWork"; import { isCrimeWork } from "../../Work/CrimeWork"; -import { ActionIdentifier } from "../../Bladeburner/ActionIdentifier"; import { Skills } from "../../PersonObjects/Skills"; import { calculateSkillProgress } from "../../PersonObjects/formulas/skill"; import { EventEmitter } from "../../utils/EventEmitter"; @@ -240,10 +232,9 @@ export function CharacterOverview({ parentOpen, save, killScripts }: OverviewPro ); } -function ActionText(props: { action: ActionIdentifier }): React.ReactElement { +function ActionText({ action }: { action: ActionIdentifier }): React.ReactElement { const bladeburner = Player.bladeburner; if (!bladeburner) return <>; - const action = bladeburner.getTypeAndNameFromActionId(props.action); return ( {action.type}: {action.name} @@ -262,9 +253,7 @@ function BladeburnerText(): React.ReactElement { const action = Player.bladeburner?.action; return useMemo( () => - //Action type 1 is Idle, see ActionTypes.ts - //TODO 2.3: Revamp typing in bladeburner - !action || action.type === 1 ? ( + !action ? ( <> ) : ( <> diff --git a/src/utils/JSONReviver.ts b/src/utils/JSONReviver.ts index cd8f8405a..6c9b66de3 100644 --- a/src/utils/JSONReviver.ts +++ b/src/utils/JSONReviver.ts @@ -1,17 +1,16 @@ /* Generic Reviver, toJSON, and fromJSON functions used for saving and loading objects */ -import type { Unknownify } from "../types"; - import { ObjectValidator, validateObject } from "./Validator"; import { JSONMap, JSONSet } from "../Types/Jsonable"; +import { loadActionIdentifier } from "../Bladeburner/utils/loadActionIdentifier"; type JsonableClass = (new () => { toJSON: () => IReviverValue }) & { fromJSON: (value: IReviverValue) => any; validationData?: ObjectValidator; }; -export interface IReviverValue { +export interface IReviverValue { ctor: string; - data: any; + data: T; } function isReviverValue(value: unknown): value is IReviverValue { return ( @@ -37,6 +36,8 @@ export function Reviver(_key: string, value: unknown): any { case "Faction": // Reviver removed in v2.6.1 console.warn(`Legacy load type ${value.ctor} converted to expected format while loading.`); return value.data; + case "ActionIdentifier": // No longer a class as of v2.6.1 + return loadActionIdentifier(value.data); } // Missing constructor with no special handling. Throw error. throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`); @@ -102,7 +103,3 @@ export function Generic_fromJSON>( for (const [key, val] of Object.entries(data) as [keyof T, T[keyof T]][]) obj[key] = val; return obj; } - -// This function is empty because Unknownify is a typesafe assertion on any object with no runtime checks needed. -// eslint-disable-next-line @typescript-eslint/no-empty-function -export function assertLoadingType(val: object): asserts val is Unknownify {} diff --git a/src/utils/TypeAssertion.ts b/src/utils/TypeAssertion.ts new file mode 100644 index 000000000..1937e4e9a --- /dev/null +++ b/src/utils/TypeAssertion.ts @@ -0,0 +1,4 @@ +import type { Unknownify } from "../types"; +// This function is empty because Unknownify is a typesafe assertion on any object with no runtime checks needed. +// eslint-disable-next-line @typescript-eslint/no-empty-function +export function assertLoadingType(val: object): asserts val is Unknownify {} diff --git a/src/utils/helpers/clampNumber.ts b/src/utils/helpers/clampNumber.ts index 9cb1a9897..5a423ba48 100644 --- a/src/utils/helpers/clampNumber.ts +++ b/src/utils/helpers/clampNumber.ts @@ -2,14 +2,22 @@ import { CONSTANTS } from "../../Constants"; /** * Clamps the value on a lower and an upper bound * @param {number} value Value to clamp - * @param {number} min Lower bound, defaults to Number.MIN_VALUE + * @param {number} min Lower bound, defaults to negative Number.MAX_VALUE * @param {number} max Upper bound, defaults to Number.MAX_VALUE * @returns {number} Clamped value */ -export function clampNumber(value: number, min = Number.MIN_VALUE, max = Number.MAX_VALUE) { +export function clampNumber(value: number, min = -Number.MAX_VALUE, max = Number.MAX_VALUE) { if (isNaN(value)) { if (CONSTANTS.isDevBranch) throw new Error("NaN passed into clampNumber()"); return min; } return Math.max(Math.min(value, max), min); } + +export function clampInteger(value: number, min = -Number.MAX_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) { + if (isNaN(value)) { + if (CONSTANTS.isDevBranch) throw new Error("NaN passed into clampInteger()"); + return min; + } + return Math.round(Math.max(Math.min(value, max), min)); +} diff --git a/test/jest/__snapshots__/FullSave.test.ts.snap b/test/jest/__snapshots__/FullSave.test.ts.snap index d710741d4..09fa89b05 100644 --- a/test/jest/__snapshots__/FullSave.test.ts.snap +++ b/test/jest/__snapshots__/FullSave.test.ts.snap @@ -58,34 +58,15 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = ` "bladeburner": { "ctor": "Bladeburner", "data": { - "action": { - "ctor": "ActionIdentifier", - "data": { - "name": "", - "type": 1, - }, - }, + "action": null, "actionTimeCurrent": 0, "actionTimeOverflow": 0, "actionTimeToComplete": 0, - "automateActionHigh": { - "ctor": "ActionIdentifier", - "data": { - "name": "", - "type": 1, - }, - }, - "automateActionLow": { - "ctor": "ActionIdentifier", - "data": { - "name": "", - "type": 1, - }, - }, + "automateActionHigh": null, + "automateActionLow": null, "automateEnabled": false, "automateThreshHigh": 0, "automateThreshLow": 0, - "blackops": {}, "cities": { "Aevum": { "ctor": "City", @@ -159,124 +140,36 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = ` "ctor": "Contract", "data": { "autoLevel": true, - "baseDifficulty": 266.2162162162162, "count": 117, - "decays": { - "agi": 0.91, - "cha": 0.8, - "def": 0.91, - "dex": 0.91, - "hack": 0, - "int": 0.9, - "str": 0.91, - }, - "difficultyFac": 1.04, "failures": 0, - "hpLoss": 1, - "hpLost": 0, - "isKill": true, - "isStealth": false, "level": 1, "maxLevel": 1, - "name": "Bounty Hunter", - "rankGain": 0.9, - "rankLoss": 0, - "rewardFac": 1.085, "successes": 0, - "teamCount": 0, - "weights": { - "agi": 0.25, - "cha": 0.1, - "def": 0.15, - "dex": 0.25, - "hack": 0, - "int": 0.1, - "str": 0.15, - }, }, }, "Retirement": { "ctor": "Contract", "data": { "autoLevel": true, - "baseDifficulty": 216.21621621621622, - "count": 129, - "decays": { - "agi": 0.91, - "cha": 0.8, - "def": 0.91, - "dex": 0.91, - "hack": 0, - "int": 0.9, - "str": 0.91, - }, - "difficultyFac": 1.03, + "count": 125, "failures": 0, - "hpLoss": 1, - "hpLost": 0, - "isKill": true, - "isStealth": false, "level": 1, "maxLevel": 1, - "name": "Retirement", - "rankGain": 0.6, - "rankLoss": 0, - "rewardFac": 1.065, "successes": 0, - "teamCount": 0, - "weights": { - "agi": 0.2, - "cha": 0.1, - "def": 0.2, - "dex": 0.2, - "hack": 0, - "int": 0.1, - "str": 0.2, - }, }, }, "Tracking": { "ctor": "Contract", "data": { "autoLevel": true, - "baseDifficulty": 131.0810810810811, - "count": 111, - "decays": { - "agi": 0.91, - "cha": 0.9, - "def": 0.91, - "dex": 0.91, - "hack": 0, - "int": 1, - "str": 0.91, - }, - "difficultyFac": 1.02, + "count": 115, "failures": 0, - "hpLoss": 0.5, - "hpLost": 0, - "isKill": false, - "isStealth": true, "level": 1, "maxLevel": 1, - "name": "Tracking", - "rankGain": 0.3, - "rankLoss": 0, - "rewardFac": 1.041, "successes": 0, - "teamCount": 0, - "weights": { - "agi": 0.35, - "cha": 0.1, - "def": 0.05, - "dex": 0.35, - "hack": 0, - "int": 0.05, - "str": 0.05, - }, }, }, }, - "hpLost": 0, "logging": { "blackops": true, "contracts": true, @@ -287,276 +180,84 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = ` "maxRank": 2000, "maxStamina": 1, "moneyLost": 0, + "numBlackOpsComplete": 0, "numHosp": 0, "operations": { "Assassination": { "ctor": "Operation", "data": { "autoLevel": true, - "baseDifficulty": 1467.5675675675675, - "count": 51, - "decays": { - "agi": 0.8, - "cha": 0, - "def": 0.8, - "dex": 0.8, - "hack": 0.6, - "int": 0.8, - "str": 0.8, - }, - "difficultyFac": 1.06, + "count": 23, "failures": 0, - "hpLoss": 5, - "hpLost": 0, - "isKill": true, - "isStealth": true, "level": 1, "maxLevel": 1, - "name": "Assassination", - "rankGain": 44, - "rankLoss": 4, - "reqdRank": 50000, - "rewardFac": 1.14, "successes": 0, "teamCount": 0, - "weights": { - "agi": 0.3, - "cha": 0, - "def": 0.1, - "dex": 0.3, - "hack": 0.1, - "int": 0.1, - "str": 0.1, - }, }, }, "Investigation": { "ctor": "Operation", "data": { "autoLevel": true, - "baseDifficulty": 438.9189189189189, - "count": 94, - "decays": { - "agi": 0.9, - "cha": 0.7, - "def": 0.9, - "dex": 0.9, - "hack": 0.85, - "int": 0.9, - "str": 0.9, - }, - "difficultyFac": 1.03, + "count": 88, "failures": 0, - "hpLoss": 0, - "hpLost": 0, - "isKill": false, - "isStealth": true, "level": 1, "maxLevel": 1, - "name": "Investigation", - "rankGain": 2.2, - "rankLoss": 0.2, - "reqdRank": 25, - "rewardFac": 1.07, "successes": 0, "teamCount": 0, - "weights": { - "agi": 0.1, - "cha": 0.25, - "def": 0.05, - "dex": 0.2, - "hack": 0.25, - "int": 0.1, - "str": 0.05, - }, }, }, "Raid": { "ctor": "Operation", "data": { "autoLevel": true, - "baseDifficulty": 756.7567567567568, - "count": 27, - "decays": { - "agi": 0.8, - "cha": 0, - "def": 0.8, - "dex": 0.8, - "hack": 0.7, - "int": 0.9, - "str": 0.8, - }, - "difficultyFac": 1.045, + "count": 7, "failures": 0, - "hpLoss": 50, - "hpLost": 0, - "isKill": true, - "isStealth": false, "level": 1, "maxLevel": 1, - "name": "Raid", - "rankGain": 55, - "rankLoss": 2.5, - "reqdRank": 3000, - "rewardFac": 1.1, "successes": 0, "teamCount": 0, - "weights": { - "agi": 0.2, - "cha": 0, - "def": 0.2, - "dex": 0.2, - "hack": 0.1, - "int": 0.1, - "str": 0.2, - }, }, }, "Stealth Retirement Operation": { "ctor": "Operation", "data": { "autoLevel": true, - "baseDifficulty": 962.1621621621622, - "count": 39, - "decays": { - "agi": 0.8, - "cha": 0, - "def": 0.8, - "dex": 0.8, - "hack": 0.7, - "int": 0.9, - "str": 0.8, - }, - "difficultyFac": 1.05, + "count": 15, "failures": 0, - "hpLoss": 10, - "hpLost": 0, - "isKill": true, - "isStealth": true, "level": 1, "maxLevel": 1, - "name": "Stealth Retirement Operation", - "rankGain": 22, - "rankLoss": 2, - "reqdRank": 20000, - "rewardFac": 1.11, "successes": 0, "teamCount": 0, - "weights": { - "agi": 0.3, - "cha": 0, - "def": 0.1, - "dex": 0.3, - "hack": 0.1, - "int": 0.1, - "str": 0.1, - }, }, }, "Sting Operation": { "ctor": "Operation", "data": { "autoLevel": true, - "baseDifficulty": 604.3243243243244, - "count": 15, - "decays": { - "agi": 0.85, - "cha": 0.7, - "def": 0.85, - "dex": 0.85, - "hack": 0.8, - "int": 0.9, - "str": 0.85, - }, - "difficultyFac": 1.04, + "count": 148, "failures": 0, - "hpLoss": 2.5, - "hpLost": 0, - "isKill": false, - "isStealth": true, "level": 1, "maxLevel": 1, - "name": "Sting Operation", - "rankGain": 5.5, - "rankLoss": 0.5, - "reqdRank": 500, - "rewardFac": 1.095, "successes": 0, "teamCount": 0, - "weights": { - "agi": 0.1, - "cha": 0.2, - "def": 0.05, - "dex": 0.25, - "hack": 0.25, - "int": 0.1, - "str": 0.05, - }, }, }, "Undercover Operation": { "ctor": "Operation", "data": { "autoLevel": true, - "baseDifficulty": 456.7567567567568, - "count": 2, - "decays": { - "agi": 0.9, - "cha": 0.7, - "def": 0.9, - "dex": 0.9, - "hack": 0.8, - "int": 0.9, - "str": 0.9, - }, - "difficultyFac": 1.04, + "count": 94, "failures": 0, - "hpLoss": 2, - "hpLost": 0, - "isKill": false, - "isStealth": true, "level": 1, "maxLevel": 1, - "name": "Undercover Operation", - "rankGain": 4.4, - "rankLoss": 0.4, - "reqdRank": 100, - "rewardFac": 1.09, "successes": 0, "teamCount": 0, - "weights": { - "agi": 0.2, - "cha": 0.2, - "def": 0.05, - "dex": 0.2, - "hack": 0.2, - "int": 0.1, - "str": 0.05, - }, }, }, }, "randomEventCounter": 303, "rank": 2000, - "skillMultipliers": { - "actionTime": 1, - "effAgi": 1, - "effCha": 1, - "effDef": 1, - "effDex": 1, - "effHack": 1, - "effInt": 1, - "effStr": 1, - "expGain": 1, - "money": 1, - "stamina": 1, - "successChanceAll": 1, - "successChanceContract": 1, - "successChanceEstimate": 1, - "successChanceKill": 1, - "successChanceOperation": 1, - "successChanceStealth": 1, - }, "skillPoints": 666, "skills": {}, "sleeveSize": 0,