From ed98896238d553aac5a58cc5108191a5e1d4b936 Mon Sep 17 00:00:00 2001 From: Vomitblood Date: Fri, 1 May 2026 20:02:08 +0800 Subject: [PATCH] reverse engineered formulas.exe --- package-lock.json | 2 +- src/utils/formulas/README | 11 + src/utils/formulas/constants.ts | 154 ++++++++ src/utils/formulas/exports.ts | 194 ++++++++++ src/utils/formulas/gang.ts | 205 +++++++++++ src/utils/formulas/hacking.ts | 309 ++++++++++++++++ src/utils/formulas/hacknet-nodes.ts | 92 +++++ src/utils/formulas/hacknet-servers.ts | 120 +++++++ src/utils/formulas/player.ts | 492 ++++++++++++++++++++++++++ src/utils/formulas/reputation.ts | 40 +++ src/utils/formulas/skills.ts | 85 +++++ src/utils/formulas/utils.ts | 25 ++ tsconfig.json | 3 +- 13 files changed, 1729 insertions(+), 3 deletions(-) create mode 100644 src/utils/formulas/README create mode 100644 src/utils/formulas/constants.ts create mode 100644 src/utils/formulas/exports.ts create mode 100644 src/utils/formulas/gang.ts create mode 100644 src/utils/formulas/hacking.ts create mode 100644 src/utils/formulas/hacknet-nodes.ts create mode 100644 src/utils/formulas/hacknet-servers.ts create mode 100644 src/utils/formulas/player.ts create mode 100644 src/utils/formulas/reputation.ts create mode 100644 src/utils/formulas/skills.ts create mode 100644 src/utils/formulas/utils.ts diff --git a/package-lock.json b/package-lock.json index 0edfec6..3981dc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4127,4 +4127,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/src/utils/formulas/README b/src/utils/formulas/README new file mode 100644 index 0000000..dd63f19 --- /dev/null +++ b/src/utils/formulas/README @@ -0,0 +1,11 @@ +- ALL UNTESTED CODE +- Reverse engineered from bitburner source + +To be reverse engineered: + +- work +- bladeburner + +To be tested: + +- everything lol diff --git a/src/utils/formulas/constants.ts b/src/utils/formulas/constants.ts new file mode 100644 index 0000000..f6e5a65 --- /dev/null +++ b/src/utils/formulas/constants.ts @@ -0,0 +1,154 @@ +/** + * Generic Game Constants + * + * Constants for specific mechanics or features will NOT be here. + */ +export const CONSTANTS = { + VersionString: "2.8.1", + isDevBranch: false, + VersionNumber: 43, + + /** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience + * and the skill level formula in Player.js. Note that all this means it that when experience hits MAX_INT, then + * the player will have this level assuming no multipliers. Multipliers can cause skills to go above this. + */ + MaxSkillLevel: 975, + + // Milliseconds per game cycle + MilliPerCycle: 200, + + // Multiplier for hacking income earned from offline scripts + OfflineHackingIncome: 0.75, + + // How much reputation is needed to join a megacorporation's faction + CorpFactionRepRequirement: 400e3, + + // Cost to travel to another city + TravelCost: 200e3, + + // Faction and Company favor-related things + BaseFavorToDonate: 150, + DonateMoneyToRepDivisor: 1e6, + + // NeuroFlux Governor Augmentation cost multiplier + NeuroFluxGovernorLevelMult: 1.14, + + NumNetscriptPorts: Number.MAX_SAFE_INTEGER, + + // Augmentation Constants + MultipleAugMultiplier: 1.9, + + // TOR Router + TorRouterCost: 200e3, + + // Hospital/Health + HospitalCostPerHp: 100e3, + + // Intelligence-related constants + IntelligenceCrimeWeight: 0.025, // Weight for how much int affects crime success rates + IntelligenceCrimeBaseExpGain: 0.05, + IntelligenceProgramBaseExpGain: 0.1, // Program required hack level divided by this to determine int exp gain + IntelligenceGraftBaseExpGain: 0.05, + IntelligenceSingFnBaseExpGain: 1.5, + + // Time-related constants + MillisecondsPer20Hours: 72000000, + GameCyclesPer20Hours: 72000000 / 200, + + MillisecondsPer10Hours: 36000000, + GameCyclesPer10Hours: 36000000 / 200, + + MillisecondsPer8Hours: 28800000, + GameCyclesPer8Hours: 28800000 / 200, + + MillisecondsPer4Hours: 14400000, + GameCyclesPer4Hours: 14400000 / 200, + + MillisecondsPer2Hours: 7200000, + GameCyclesPer2Hours: 7200000 / 200, + + MillisecondsPerHour: 3600000, + GameCyclesPerHour: 3600000 / 200, + + MillisecondsPerHalfHour: 1800000, + GameCyclesPerHalfHour: 1800000 / 200, + + MillisecondsPerQuarterHour: 900000, + GameCyclesPerQuarterHour: 900000 / 200, + + MillisecondsPerTenMinutes: 600000, + + MillisecondsPerFiveMinutes: 300000, + GameCyclesPerFiveMinutes: 300000 / 200, + + // Player Work & Action + BaseFocusBonus: 0.8, + + // Coding Contract + // TODO: Move this into Coding contract implementation? + CodingContractBaseFactionRepGain: 2500, + CodingContractBaseCompanyRepGain: 4000, + CodingContractBaseMoneyGain: 75e6, + + // Augmentation grafting multipliers + AugmentationGraftingCostMult: 3, + AugmentationGraftingTimeBase: 3600000, + + // SoA mults + SoACostMult: 7, + SoARepMult: 1.3, + + // Value raised to the number of entropy stacks, then multiplied to player multipliers + EntropyEffect: 0.98, + + // Number of blood, plasma, or platelet donations the developer has verified. Boosts NFG. + Donations: 179, + + // Only use this if a backdoor is installed in the company's server + CompanyRequiredReputationMultiplier: 0.75, + + // Also update Documentation/doc/changelog.md when appropriate (when doing a release) + LatestUpdate: `asdf`, +} as const; + +export const gameCPS = 1000 / CONSTANTS.MilliPerCycle; // 5 cycles per second + +export const HacknetNodeConstants = { + MoneyGainPerLevel: 1.5, + + BaseCost: 1000, + LevelBaseCost: 500, + RamBaseCost: 30e3, + CoreBaseCost: 500e3, + + PurchaseNextMult: 1.85, + UpgradeLevelMult: 1.04, + UpgradeRamMult: 1.28, + UpgradeCoreMult: 1.48, + + MaxLevel: 200, + MaxRam: 64, + MaxCores: 16, +} as const; + +export const HacknetServerConstants = { + HashesPerLevel: 0.001, + + BaseCost: 50e3, + RamBaseCost: 200e3, + CoreBaseCost: 1e6, + CacheBaseCost: 10e6, + + PurchaseMult: 3.2, + UpgradeLevelMult: 1.1, + UpgradeRamMult: 1.4, + UpgradeCoreMult: 1.55, + UpgradeCacheMult: 1.85, + + MaxServers: 20, + + MaxLevel: 300, + MaxRam: 8192, + MaxCores: 128, + MaxCache: 15, +} as const; diff --git a/src/utils/formulas/exports.ts b/src/utils/formulas/exports.ts new file mode 100644 index 0000000..57f6939 --- /dev/null +++ b/src/utils/formulas/exports.ts @@ -0,0 +1,194 @@ +import { Player } from "./player"; +import { clampNumber } from "./utils"; + +export type PartialRecord = Partial>; +export const getRecordEntries = Object.entries as (record: PartialRecord) => [K, V][]; + +/** + * Bitnode multipliers influence the difficulty of different aspects of the game. + * Each Bitnode has a different theme/strategy to achieving the end goal, so these multipliers will can help drive the + * player toward the intended strategy. Unless they really want to play the long, slow game of waiting... + */ +export class BitNodeMultipliers { + /** Influences how quickly the player's agility level (not exp) scales */ + AgilityLevelMultiplier = 1; + + /** Influences the base cost to purchase an augmentation. */ + AugmentationMoneyCost = 1; + + /** Influences the base rep the player must have with a faction to purchase an augmentation. */ + AugmentationRepCost = 1; + + /** Influences how quickly the player can gain rank within Bladeburner. */ + BladeburnerRank = 1; + + /** Influences the cost of skill levels from Bladeburner. */ + BladeburnerSkillCost = 1; + + /** Influences how quickly the player's charisma level (not exp) scales */ + CharismaLevelMultiplier = 1; + + /** Influences the experience gained for each ability when a player completes a class. */ + ClassGymExpGain = 1; + + /** Influences the amount of money gained from completing Coding Contracts. */ + CodingContractMoney = 1; + + /** Influences the experience gained for each ability when the player completes working their job. */ + CompanyWorkExpGain = 1; + + /** Influences how much money the player earns when completing working their job. */ + CompanyWorkMoney = 1; + + /** Influences how much rep the player gains when performing work for a company. */ + CompanyWorkRepGain = 1; + + /** Influences the amount of divisions a corporation can have at the same time. */ + CorporationDivisions = 1; + + /** Influences profits from corporation dividends and selling shares. */ + CorporationSoftcap = 1; + + /** Influences the valuation of corporations created by the player. */ + CorporationValuation = 1; + + /** Influences the base experience gained for each ability when the player commits a crime. */ + CrimeExpGain = 1; + + /** Influences the base money gained when the player commits a crime. */ + CrimeMoney = 1; + + /** Influences the success chance of committing crimes */ + CrimeSuccessRate = 1; + + /** Influences how many Augmentations you need in order to get invited to the Daedalus faction */ + DaedalusAugsRequirement = 30; + + /** Influences how quickly the player's defense level (not exp) scales */ + DefenseLevelMultiplier = 1; + + /** Influences how quickly the player's dexterity level (not exp) scales */ + DexterityLevelMultiplier = 1; + + /** Influences how much rep the player gains in each faction simply by being a member. */ + FactionPassiveRepGain = 1; + + /** Influences the experience gained for each ability when the player completes work for a Faction. */ + FactionWorkExpGain = 1; + + /** Influences how much rep the player gains when performing work for a faction or donating to it. */ + FactionWorkRepGain = 1; + + /** Influences how much it costs to unlock the stock market's 4S Market Data API */ + FourSigmaMarketDataApiCost = 1; + + /** Influences how much it costs to unlock the stock market's 4S Market Data (NOT API) */ + FourSigmaMarketDataCost = 1; + + /** Influences the respect gain and money gain of your gang. */ + GangSoftcap = 1; + + /** Percentage of unique augs that the gang has. */ + GangUniqueAugs = 1; + + /** Percentage multiplier on the effect of the IPvGO rewards **/ + GoPower = 1; + + /** Influences the experienced gained when hacking a server. */ + HackExpGain = 1; + + /** Influences how quickly the player's hacking level (not experience) scales */ + HackingLevelMultiplier = 1; + + /** Influences how quickly the player's hack(), grow() and weaken() calls run */ + HackingSpeedMultiplier = 1; + + /** + * Influences how much money is produced by Hacknet Nodes. + * Influences the hash rate of Hacknet Servers (unlocked in BitNode-9) + */ + HacknetNodeMoney = 1; + + /** Influences how much money it costs to upgrade your home computer's RAM */ + HomeComputerRamCost = 1; + + /** Influences how much money is gained when the player infiltrates a company. */ + InfiltrationMoney = 1; + + /** Influences how much rep the player can gain from factions when selling stolen documents and secrets */ + InfiltrationRep = 1; + + /** + * Influences how much money the player actually gains when they hack a server via the terminal. This is different + * from ScriptHackMoney. When the player hack a server via the terminal, the amount of money in that server is + * reduced, but they do not gain that same amount. + */ + ManualHackMoney = 1; + + /** Influence how much it costs to purchase a server */ + PurchasedServerCost = 1; + + /** Influence how much it costs to purchase a server */ + PurchasedServerSoftcap = 1; + + /** Influences the maximum number of purchased servers you can have */ + PurchasedServerLimit = 1; + + /** Influences the maximum allowed RAM for a purchased server */ + PurchasedServerMaxRam = 1; + + /** Influences the minimum favor the player must have with a faction before they can donate to gain rep. */ + RepToDonateToFaction = 1; + + /** Influences how much money is stolen from a server when the player performs a hack against it. */ + ScriptHackMoney = 1; + + /** + * Influences how much money the player actually gains when a script hacks a server. This is different from + * ScriptHackMoney. When a script hacks a server, the amount of money in that server is reduced, but the player does + * not gain that same amount. + */ + ScriptHackMoneyGain = 1; + + /** Influences the growth percentage per cycle against a server. */ + ServerGrowthRate = 1; + + /** Influences the maximum money that a server can grow to. */ + ServerMaxMoney = 1; + + /** Influences the initial money that a server starts with. */ + ServerStartingMoney = 1; + + /** Influences the initial security level (hackDifficulty) of a server. */ + ServerStartingSecurity = 1; + + /** Influences the weaken amount per invocation against a server. */ + ServerWeakenRate = 1; + + /** Influences how quickly the player's strength level (not exp) scales */ + StrengthLevelMultiplier = 1; + + /** Influences the power of the gift. */ + StaneksGiftPowerMultiplier = 1; + + /** Influences the size of the gift. */ + StaneksGiftExtraSize = 0; + + /** Influences the hacking skill required to backdoor the world daemon. */ + WorldDaemonDifficulty = 1; + + constructor(a: PartialRecord = {}) { + for (const [key, value] of getRecordEntries(a)) this[key] = clampNumber(value); + } +} + +/** The multipliers currently in effect */ +export const currentNodeMults = new BitNodeMultipliers(); + +export function calculateIntelligenceBonus(intelligence: number, weight = 1): number { + const effectiveIntelligence = + Player.bitNodeOptions.intelligenceOverride !== undefined + ? Math.min(Player.bitNodeOptions.intelligenceOverride, intelligence) + : intelligence; + return 1 + (weight * Math.pow(effectiveIntelligence, 0.8)) / 600; +} diff --git a/src/utils/formulas/gang.ts b/src/utils/formulas/gang.ts new file mode 100644 index 0000000..d6a087e --- /dev/null +++ b/src/utils/formulas/gang.ts @@ -0,0 +1,205 @@ +import { currentNodeMults } from "./exports"; + +export interface FormulaGang { + respect: number; + territory: number; + wantedLevel: number; +} + +export interface ITerritory { + money: number; + respect: number; + wanted: number; +} + +export interface ITaskParams { + baseRespect?: number; + baseWanted?: number; + baseMoney?: number; + hackWeight?: number; + strWeight?: number; + defWeight?: number; + dexWeight?: number; + agiWeight?: number; + chaWeight?: number; + difficulty?: number; + territory?: ITerritory; +} + +export class GangMember { + name: string; + task = "Unassigned"; + + earnedRespect = 0; + + hack = 1; + str = 1; + def = 1; + dex = 1; + agi = 1; + cha = 1; + + hack_exp = 0; + str_exp = 0; + def_exp = 0; + dex_exp = 0; + agi_exp = 0; + cha_exp = 0; + + hack_mult = 1; + str_mult = 1; + def_mult = 1; + dex_mult = 1; + agi_mult = 1; + cha_mult = 1; + + hack_asc_points = 0; + str_asc_points = 0; + def_asc_points = 0; + dex_asc_points = 0; + agi_asc_points = 0; + cha_asc_points = 0; + + upgrades: string[] = []; // Names of upgrades + augmentations: string[] = []; // Names of augmentations only + + constructor(name = "") { + this.name = name; + } +} + +export class GangMemberTask { + name: string; + desc: string; + + isHacking: boolean; + isCombat: boolean; + + baseRespect: number; + baseWanted: number; + baseMoney: number; + + hackWeight: number; + strWeight: number; + defWeight: number; + dexWeight: number; + agiWeight: number; + chaWeight: number; + + difficulty: number; + + territory: ITerritory; + + // Defines tasks that Gang Members can work on + constructor(name: string, desc: string, isHacking: boolean, isCombat: boolean, params: ITaskParams) { + this.name = name; + this.desc = desc; + + // Flags that describe whether this Task is applicable for Hacking/Combat gangs + this.isHacking = isHacking; + this.isCombat = isCombat; + + // Base gain rates for respect/wanted/money + this.baseRespect = params.baseRespect ? params.baseRespect : 0; + this.baseWanted = params.baseWanted ? params.baseWanted : 0; + this.baseMoney = params.baseMoney ? params.baseMoney : 0; + + // Weighting for the effect that each stat has on the tasks effectiveness. + // Weights must add up to 100 + this.hackWeight = params.hackWeight ? params.hackWeight : 0; + this.strWeight = params.strWeight ? params.strWeight : 0; + this.defWeight = params.defWeight ? params.defWeight : 0; + this.dexWeight = params.dexWeight ? params.dexWeight : 0; + this.agiWeight = params.agiWeight ? params.agiWeight : 0; + this.chaWeight = params.chaWeight ? params.chaWeight : 0; + + if ( + Math.round( + this.hackWeight + this.strWeight + this.defWeight + this.dexWeight + this.agiWeight + this.chaWeight, + ) != 100 + ) { + console.error(`GangMemberTask ${this.name} weights do not add up to 100`); + } + + // 1 - 100 + this.difficulty = params.difficulty ? params.difficulty : 1; + + // Territory Factors. Exponential factors that dictate how territory affects gains + // Formula: Territory Multiplier = (Territory * 100) ^ factor / 100 + // So factor should be > 1 if something should scale exponentially with territory + // and should be < 1 if it should have diminishing returns + this.territory = params.territory ? params.territory : { money: 1, respect: 1, wanted: 1 }; + } +} + +export function calculateWantedPenalty(gang: FormulaGang): number { + return gang.respect / (gang.respect + gang.wantedLevel); +} + +export function calculateRespectGain(gang: FormulaGang, member: GangMember, task: GangMemberTask): number { + if (task.baseRespect === 0) return 0; + let statWeight = + (task.hackWeight / 100) * member.hack + + (task.strWeight / 100) * member.str + + (task.defWeight / 100) * member.def + + (task.dexWeight / 100) * member.dex + + (task.agiWeight / 100) * member.agi + + (task.chaWeight / 100) * member.cha; + statWeight -= 4 * task.difficulty; + if (statWeight <= 0) return 0; + const territoryMult = Math.max(0.005, Math.pow(gang.territory * 100, task.territory.respect) / 100); + const territoryPenalty = (0.2 * gang.territory + 0.8) * currentNodeMults.GangSoftcap; + if (isNaN(territoryMult) || territoryMult <= 0) return 0; + const respectMult = calculateWantedPenalty(gang); + return Math.pow(11 * task.baseRespect * statWeight * territoryMult * respectMult, territoryPenalty); +} + +export function calculateWantedLevelGain(gang: FormulaGang, member: GangMember, task: GangMemberTask): number { + if (task.baseWanted === 0) return 0; + let statWeight = + (task.hackWeight / 100) * member.hack + + (task.strWeight / 100) * member.str + + (task.defWeight / 100) * member.def + + (task.dexWeight / 100) * member.dex + + (task.agiWeight / 100) * member.agi + + (task.chaWeight / 100) * member.cha; + statWeight -= 3.5 * task.difficulty; + if (statWeight <= 0) return 0; + const territoryMult = Math.max(0.005, Math.pow(gang.territory * 100, task.territory.wanted) / 100); + if (isNaN(territoryMult) || territoryMult <= 0) return 0; + if (task.baseWanted < 0) { + return 0.4 * task.baseWanted * statWeight * territoryMult; + } + const calc = (7 * task.baseWanted) / Math.pow(3 * statWeight * territoryMult, 0.8); + + // Put an arbitrary cap on this to prevent wanted level from rising too fast if the + // denominator is very small. Might want to rethink formula later + return Math.min(100, calc); +} + +export function calculateMoneyGain(gang: FormulaGang, member: GangMember, task: GangMemberTask): number { + if (task.baseMoney === 0) return 0; + let statWeight = + (task.hackWeight / 100) * member.hack + + (task.strWeight / 100) * member.str + + (task.defWeight / 100) * member.def + + (task.dexWeight / 100) * member.dex + + (task.agiWeight / 100) * member.agi + + (task.chaWeight / 100) * member.cha; + + statWeight -= 3.2 * task.difficulty; + if (statWeight <= 0) return 0; + const territoryMult = Math.max(0.005, Math.pow(gang.territory * 100, task.territory.money) / 100); + if (isNaN(territoryMult) || territoryMult <= 0) return 0; + const respectMult = calculateWantedPenalty(gang); + const territoryPenalty = (0.2 * gang.territory + 0.8) * currentNodeMults.GangSoftcap; + return Math.pow(5 * task.baseMoney * statWeight * territoryMult * respectMult, territoryPenalty); +} + +export function calculateAscensionPointsGain(exp: number): number { + return Math.max(exp - 1000, 0); +} + +export function calculateAscensionMult(points: number): number { + return Math.max(Math.pow(points / 2000, 0.5), 1); +} diff --git a/src/utils/formulas/hacking.ts b/src/utils/formulas/hacking.ts new file mode 100644 index 0000000..62e5063 --- /dev/null +++ b/src/utils/formulas/hacking.ts @@ -0,0 +1,309 @@ +import { Player as IPerson, Server as IServer } from "@ns"; +import { currentNodeMults } from "./exports"; +import { clampNumber } from "./utils"; +import { Player } from "./player"; + +export const ServerConstants = { + // Base RAM costs + BaseCostFor1GBOfRamHome: 32000, + BaseCostFor1GBOfRamServer: 55000, //1 GB of RAM + // Server-related constants + HomeComputerMaxRam: 1073741824, // 2 ^ 30 + ServerBaseGrowthIncr: 0.03, // Unadjusted growth increment (growth rate is this * adjustment + 1) + ServerMaxGrowthLog: 0.00349388925425578, // Maximum possible growth rate accounting for server security, precomputed as log1p(.0035) + ServerFortifyAmount: 0.002, // Amount by which server's security increases when its hacked/grown + ServerWeakenAmount: 0.05, // Amount by which server's security decreases when weakened + + PurchasedServerLimit: 25, + PurchasedServerMaxRam: 1048576, // 2^20 +} as const; + +/** + * Checks that a variable is a valid number. A valid number + * must be a "number" type and cannot be NaN + */ +export function isValidNumber(n: number): boolean { + return typeof n === "number" && !isNaN(n); +} + +export function calculateIntelligenceBonus(intelligence: number, weight = 1): number { + const effectiveIntelligence = + Player.bitNodeOptions.intelligenceOverride !== undefined + ? Math.min(Player.bitNodeOptions.intelligenceOverride, intelligence) + : intelligence; + return 1 + (weight * Math.pow(effectiveIntelligence, 0.8)) / 600; +} + +/** Returns the chance the person has to successfully hack a server */ +export function calculateHackingChance(server: IServer, person: IPerson): number { + const hackDifficulty = server.hackDifficulty ?? 100; + const requiredHackingSkill = server.requiredHackingSkill ?? 1e9; + // Unrooted or unhackable server + if (!server.hasAdminRights || hackDifficulty >= 100) return 0; + const hackFactor = 1.75; + const difficultyMult = (100 - hackDifficulty) / 100; + const skillMult = clampNumber(hackFactor * person.skills.hacking, 1); + const skillChance = (skillMult - requiredHackingSkill) / skillMult; + const chance = + skillChance * + difficultyMult * + person.mults.hacking_chance * + calculateIntelligenceBonus(person.skills.intelligence, 1); + return clampNumber(chance, 0, 1); +} + +/** + * Returns the amount of hacking experience the person will gain upon + * successfully hacking a server + */ +export function calculateHackingExpGain(server: IServer, person: IPerson): number { + const baseDifficulty = server.baseDifficulty; + if (!baseDifficulty) return 0; + const baseExpGain = 3; + const diffFactor = 0.3; + let expGain = baseExpGain; + expGain += baseDifficulty * diffFactor; + return expGain * person.mults.hacking_exp * currentNodeMults.HackExpGain; +} + +/** + * Returns the percentage of money that will be stolen from a server if + * it is successfully hacked (returns the decimal form, not the actual percent value) + */ +export function calculatePercentMoneyHacked(server: IServer, person: IPerson): number { + const hackDifficulty = server.hackDifficulty ?? 100; + if (hackDifficulty >= 100) return 0; + const requiredHackingSkill = server.requiredHackingSkill ?? 1e9; + // Adjust if needed for balancing. This is the divisor for the final calculation + const balanceFactor = 240; + + const difficultyMult = (100 - hackDifficulty) / 100; + const skillMult = (person.skills.hacking - (requiredHackingSkill - 1)) / person.skills.hacking; + const percentMoneyHacked = + (difficultyMult * skillMult * person.mults.hacking_money * currentNodeMults.ScriptHackMoney) / balanceFactor; + + return Math.min(1, Math.max(percentMoneyHacked, 0)); +} + +/** Returns time it takes to complete a hack on a server, in seconds */ +export function calculateHackingTime(server: IServer, person: IPerson): number { + const { hackDifficulty, requiredHackingSkill } = server; + if (typeof hackDifficulty !== "number" || typeof requiredHackingSkill !== "number") return Infinity; + const difficultyMult = requiredHackingSkill * hackDifficulty; + + const baseDiff = 500; + const baseSkill = 50; + const diffFactor = 2.5; + let skillFactor = diffFactor * difficultyMult + baseDiff; + skillFactor /= person.skills.hacking + baseSkill; + + const hackTimeMultiplier = 5; + const hackingTime = + (hackTimeMultiplier * skillFactor) / + (person.mults.hacking_speed * + currentNodeMults.HackingSpeedMultiplier * + calculateIntelligenceBonus(person.skills.intelligence, 1)); + + return hackingTime; +} + +/** Returns time it takes to complete a grow operation on a server, in seconds */ +export function calculateGrowTime(server: IServer, person: IPerson): number { + const growTimeMultiplier = 3.2; // Relative to hacking time. 16/5 = 3.2 + + return growTimeMultiplier * calculateHackingTime(server, person); +} + +/** Returns time it takes to complete a weaken operation on a server, in seconds */ +export function calculateWeakenTime(server: IServer, person: IPerson): number { + const weakenTimeMultiplier = 4; // Relative to hacking time + + return weakenTimeMultiplier * calculateHackingTime(server, person); +} + +// Returns the log of the growth rate. When passing 1 for threads, this gives a useful constant. +export function calculateServerGrowthLog(server: IServer, threads: number, p: IPerson, cores = 1): number { + if (!server.serverGrowth) return -Infinity; + const hackDifficulty = server.hackDifficulty ?? 100; + const numServerGrowthCycles = Math.max(threads, 0); + + //Get adjusted growth log, which accounts for server security + //log1p computes log(1+p), it is far more accurate for small values. + let adjGrowthLog = Math.log1p(ServerConstants.ServerBaseGrowthIncr / hackDifficulty); + if (adjGrowthLog >= ServerConstants.ServerMaxGrowthLog) { + adjGrowthLog = ServerConstants.ServerMaxGrowthLog; + } + + //Calculate adjusted server growth rate based on parameters + const serverGrowthPercentage = server.serverGrowth / 100; + const serverGrowthPercentageAdjusted = serverGrowthPercentage * currentNodeMults.ServerGrowthRate; + + //Apply serverGrowth for the calculated number of growth cycles + const coreBonus = 1 + (cores - 1) * (1 / 16); + // It is critical that numServerGrowthCycles (aka threads) is multiplied last, + // so that it rounds the same way as numCycleForGrowthCorrected. + return adjGrowthLog * serverGrowthPercentageAdjusted * p.mults.hacking_grow * coreBonus * numServerGrowthCycles; +} + +export function calculateServerGrowth(server: IServer, threads: number, p: IPerson, cores = 1): number { + if (!server.serverGrowth) return 0; + return Math.exp(calculateServerGrowthLog(server, threads, p, cores)); +} + +// This differs from calculateServerGrowth in that it includes the additive +// factor and all the boundary checks. +export function calculateGrowMoney(server: IServer, threads: number, p: IPerson, cores = 1): number { + let serverGrowth = calculateServerGrowth(server, threads, p, cores); + if (serverGrowth < 1) { + console.warn("serverGrowth calculated to be less than 1"); + serverGrowth = 1; + } + + let moneyAvailable = server.moneyAvailable ?? Number.NaN; + moneyAvailable += threads; // It can be grown even if it has no money + moneyAvailable *= serverGrowth; + + // cap at max (or data corruption) + if ( + server.moneyMax !== undefined && + isValidNumber(server.moneyMax) && + (moneyAvailable > server.moneyMax || isNaN(moneyAvailable)) + ) { + moneyAvailable = server.moneyMax; + } + return moneyAvailable; +} + +/** + * Returns the number of "growth cycles" needed to grow the specified server by the specified amount, taking into + * account only the multiplicative factor. Does not account for the additive $1/thread. Only used for growthAnalyze. + * @param server - Server being grown + * @param growth - How much the server is being grown by, in DECIMAL form (e.g. 1.5 rather than 50) + * @param p - Reference to Player object + * @returns Number of "growth cycles" needed + */ +export function numCycleForGrowth(server: IServer, growth: number, cores = 1): number { + if (!server.serverGrowth) return Infinity; + return Math.log(growth) / calculateServerGrowthLog(server, 1, Player, cores); +} + +/** + * This function calculates the number of threads needed to grow a server from one $amount to a higher $amount + * (ie, how many threads to grow this server from $200 to $600 for example). + * It protects the inputs (so putting in INFINITY for targetMoney will use moneyMax, putting in a negative for start will use 0, etc.) + * @param server - Server being grown + * @param targetMoney - How much you want the server grown TO (not by), for instance, to grow from 200 to 600, input 600 + * @param startMoney - How much you are growing the server from, for instance, to grow from 200 to 600, input 200 + * @param cores - Number of cores on the host performing grow + * @returns Integer threads needed by a single ns.grow call to reach targetMoney from startMoney. + */ +export function numCycleForGrowthCorrected( + server: IServer, + targetMoney: number, + startMoney: number, + cores = 1, + person: IPerson = Player, +): number { + if (!server.serverGrowth) return Infinity; + const moneyMax = server.moneyMax ?? 1; + + if (startMoney < 0) startMoney = 0; // servers "can't" have less than 0 dollars on them + if (targetMoney > moneyMax) targetMoney = moneyMax; // can't grow a server to more than its moneyMax + if (targetMoney <= startMoney) return 0; // no growth --> no threads + + const k = calculateServerGrowthLog(server, 1, person, cores); + /* To understand what is done below we need to do some math. I hope the explanation is clear enough. + * First of, the names will be shortened for ease of manipulation: + * n:= targetMoney (n for new), o:= startMoney (o for old), k:= calculateServerGrowthLog, x:= threads + * x is what we are trying to compute. + * + * After growing, the money on a server is n = (o + x) * exp(k*x) + * x appears in an exponent and outside it, this is usually solved using the productLog/lambert's W special function, + * but it turns out that due to floating-point range issues this approach is *useless* to us, so it will be ignored. + * + * Instead, we proceed directly to Newton-Raphson iteration. We first rewrite the equation in + * log-form, since iterating it this way has faster convergence: log(n) = log(o+x) + k*x. + * Now our goal is to find the zero of f(x) = log((o+x)/n) + k*x. + * (Due to the shape of the function, there will be a single zero.) + * + * The idea of this method is to take the horizontal position at which the horizontal axis + * intersects with of the tangent of the function's curve as the next approximation. + * It is equivalent to treating the curve as a line (it is called a first order approximation) + * If the current approximation is x then the new approximated value is x - f(x)/f'(x) + * (where f' is the derivative of f). + * + * In our case f(x) = log((o+x)/n) + k*x, f'(x) = d(log((o+x)/n) + k*x)/dx + * = 1/(o + x) + k + * And the update step is x[new] = x - (log((o+x)/n) + k*x)/(1/(o+x) + k) + * We can simplify this by bringing the first term up into the fraction: + * = (x * (1/(o+x) + k) - log((o+x)/n) - k*x) / (1/(o+x) + k) + * = (x/(o+x) - log((o+x)/n)) / (1/(o+x) + k) [multiplying top and bottom by (o+x)] + * = (x - (o+x)*log((o+x)/n)) / (1 + (o+x)*k) + * + * The main question to ask when using this method is "does it converge?" + * (are the approximations getting better?), if it does then it does quickly. + * Since the derivative is always positive but also strictly decreasing, convergence is guaranteed. + * This also provides the useful knowledge that any x which starts *greater* than the solution will + * undershoot across to the left, while values *smaller* than the zero will continue to find + * closer approximations that are still smaller than the final value. + * + * Of great importance for reducing the number of iterations is starting with a good initial + * guess. We use a very simple starting condition: x_0 = n - o. We *know* this will always overshot + * the target, usually by a vast amount. But we can run it manually through one Newton iteration + * to get a better start with nice properties: + * x_1 = ((n - o) - (n - o + o)*log((n-o+o)/n)) / (1 + (n-o+o)*k) + * = ((n - o) - n * log(n/n)) / (1 + n*k) + * = ((n - o) - n * 0) / (1 + n*k) + * = (n - o) / (1 + n*k) + * We can do the same procedure with the exponential form of Newton's method, starting from x_0 = 0. + * This gives x_1 = (n - o) / (1 + o*k), (full derivation omitted) which will be an overestimate. + * We use a weighted average of the denominators to get the final guess: + * x = (n - o) / (1 + (1/16*n + 15/16*o)*k) + * The reason for this particular weighting is subtle; it is exactly representable and holds up + * well under a wide variety of conditions, making it likely that the we start within 1 thread of + * correct. It particularly bounds the worst-case to 3 iterations, and gives a very wide swatch + * where 2 iterations is good enough. + * + * The accuracy of the initial guess is good for many inputs - often one iteration + * is sufficient. This means the overall cost is two logs (counting the one in calculateServerGrowthLog), + * possibly one exp, 5 divisions, and a handful of basic arithmetic. + */ + const guess = (targetMoney - startMoney) / (1 + (targetMoney * (1 / 16) + startMoney * (15 / 16)) * k); + let x = guess; + let diff; + do { + const ox = startMoney + x; + // Have to use division instead of multiplication by inverse, because + // if targetMoney is MIN_VALUE then inverting gives Infinity + const newx = (x - ox * Math.log(ox / targetMoney)) / (1 + ox * k); + diff = newx - x; + x = newx; + } while (diff < -1 || diff > 1); + /* If we see a diff of 1 or less we know all future diffs will be smaller, and the rate of + * convergence means the *sum* of the diffs will be less than 1. + + * In most cases, our result here will be ceil(x). + */ + const ccycle = Math.ceil(x); + if (ccycle - x > 0.999999) { + // Rounding-error path: It's possible that we slightly overshot the integer value due to + // rounding error, and more specifically precision issues with log and the size difference of + // startMoney vs. x. See if a smaller integer works. Most of the time, x was not close enough + // that we need to try. + const fcycle = ccycle - 1; + if (targetMoney <= (startMoney + fcycle) * Math.exp(k * fcycle)) { + return fcycle; + } + } + if (ccycle >= x + ((diff <= 0 ? -diff : diff) + 0.000001)) { + // Fast-path: We know the true value is somewhere in the range [x, x + |diff|] but the next + // greatest integer is past this. Since we have to round up grows anyway, we can return this + // with no more calculation. We need some slop due to rounding errors - we can't fast-path + // a value that is too small. + return ccycle; + } + if (targetMoney <= (startMoney + ccycle) * Math.exp(k * ccycle)) { + return ccycle; + } + return ccycle + 1; +} diff --git a/src/utils/formulas/hacknet-nodes.ts b/src/utils/formulas/hacknet-nodes.ts new file mode 100644 index 0000000..b50652c --- /dev/null +++ b/src/utils/formulas/hacknet-nodes.ts @@ -0,0 +1,92 @@ +import { currentNodeMults } from "./exports"; +import { HacknetNodeConstants } from "./constants"; + +export function calculateMoneyGainRate(level: number, ram: number, cores: number, mult: number): number { + const gainPerLevel = HacknetNodeConstants.MoneyGainPerLevel; + + const levelMult = level * gainPerLevel; + const ramMult = Math.pow(1.035, ram - 1); + const coresMult = (cores + 5) / 6; + return levelMult * ramMult * coresMult * mult * currentNodeMults.HacknetNodeMoney; +} + +export function calculateLevelUpgradeCost(startingLevel: number, extraLevels = 1, costMult = 1): number { + const sanitizedLevels = Math.round(extraLevels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (startingLevel + sanitizedLevels > HacknetNodeConstants.MaxLevel) { + return Infinity; + } + + const mult = HacknetNodeConstants.UpgradeLevelMult; + let totalMultiplier = 0; + let currLevel = startingLevel - 1; + for (let i = 0; i < sanitizedLevels; ++i) { + totalMultiplier += Math.pow(mult, currLevel); + ++currLevel; + } + + return HacknetNodeConstants.LevelBaseCost * totalMultiplier * costMult; +} + +export function calculateRamUpgradeCost(startingRam: number, extraLevels = 1, costMult = 1): number { + const sanitizedLevels = Math.round(extraLevels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (startingRam * Math.pow(2, sanitizedLevels) > HacknetNodeConstants.MaxRam) { + return Infinity; + } + + let totalCost = 0; + let numUpgrades = Math.round(Math.log2(startingRam)); + let currentRam = startingRam; + + for (let i = 0; i < sanitizedLevels; ++i) { + const baseCost = currentRam * HacknetNodeConstants.RamBaseCost; + const mult = Math.pow(HacknetNodeConstants.UpgradeRamMult, numUpgrades); + + totalCost += baseCost * mult; + + currentRam *= 2; + ++numUpgrades; + } + + totalCost *= costMult; + + return totalCost; +} + +export function calculateCoreUpgradeCost(startingCores: number, extraLevels = 1, costMult = 1): number { + const sanitizedCores = Math.round(extraLevels); + if (isNaN(sanitizedCores) || sanitizedCores < 1) { + return 0; + } + + if (startingCores + sanitizedCores > HacknetNodeConstants.MaxCores) { + return Infinity; + } + + const coreBaseCost = HacknetNodeConstants.CoreBaseCost; + const mult = HacknetNodeConstants.UpgradeCoreMult; + let totalCost = 0; + let currentCores = startingCores; + for (let i = 0; i < sanitizedCores; ++i) { + totalCost += coreBaseCost * Math.pow(mult, currentCores - 1); + ++currentCores; + } + + totalCost *= costMult; + + return totalCost; +} + +export function calculateNodeCost(n: number, mult = 1): number { + if (n <= 0) { + return 0; + } + return HacknetNodeConstants.BaseCost * Math.pow(HacknetNodeConstants.PurchaseNextMult, n - 1) * mult; +} diff --git a/src/utils/formulas/hacknet-servers.ts b/src/utils/formulas/hacknet-servers.ts new file mode 100644 index 0000000..0e2e0da --- /dev/null +++ b/src/utils/formulas/hacknet-servers.ts @@ -0,0 +1,120 @@ +import { currentNodeMults } from "./exports"; +import { HacknetServerConstants } from "./constants"; + +export function calculateHashGainRate( + level: number, + ramUsed: number, + maxRam: number, + cores: number, + mult: number, +): number { + const baseGain = HacknetServerConstants.HashesPerLevel * level; + const ramMultiplier = Math.pow(1.07, Math.log2(maxRam)); + const coreMultiplier = 1 + (cores - 1) / 5; + const ramRatio = 1 - ramUsed / maxRam; + + return baseGain * ramMultiplier * coreMultiplier * ramRatio * mult * currentNodeMults.HacknetNodeMoney; +} + +export function calculateLevelUpgradeCost(startingLevel: number, extraLevels = 1, costMult = 1): number { + const sanitizedLevels = Math.round(extraLevels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (startingLevel + sanitizedLevels > HacknetServerConstants.MaxLevel) { + return Infinity; + } + + const mult = HacknetServerConstants.UpgradeLevelMult; + let totalMultiplier = 0; + let currLevel = startingLevel; + for (let i = 0; i < sanitizedLevels; ++i) { + totalMultiplier += Math.pow(mult, currLevel); + ++currLevel; + } + + return 10 * HacknetServerConstants.BaseCost * totalMultiplier * costMult; +} + +export function calculateRamUpgradeCost(startingRam: number, extraLevels = 1, costMult = 1): number { + const sanitizedLevels = Math.round(extraLevels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (startingRam * Math.pow(2, sanitizedLevels) > HacknetServerConstants.MaxRam) { + return Infinity; + } + + let totalCost = 0; + let numUpgrades = Math.round(Math.log2(startingRam)); + let currentRam = startingRam; + for (let i = 0; i < sanitizedLevels; ++i) { + const baseCost = currentRam * HacknetServerConstants.RamBaseCost; + const mult = Math.pow(HacknetServerConstants.UpgradeRamMult, numUpgrades); + + totalCost += baseCost * mult; + + currentRam *= 2; + ++numUpgrades; + } + totalCost *= costMult; + + return totalCost; +} + +export function calculateCoreUpgradeCost(startingCores: number, extraLevels = 1, costMult = 1): number { + const sanitizedLevels = Math.round(extraLevels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (startingCores + sanitizedLevels > HacknetServerConstants.MaxCores) { + return Infinity; + } + + const mult = HacknetServerConstants.UpgradeCoreMult; + let totalCost = 0; + let currentCores = startingCores; + for (let i = 0; i < sanitizedLevels; ++i) { + totalCost += Math.pow(mult, currentCores - 1); + ++currentCores; + } + totalCost *= HacknetServerConstants.CoreBaseCost; + totalCost *= costMult; + + return totalCost; +} + +export function calculateCacheUpgradeCost(startingCache: number, extraLevels = 1): number { + const sanitizedLevels = Math.round(extraLevels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (startingCache + sanitizedLevels > HacknetServerConstants.MaxCache) { + return Infinity; + } + + const mult = HacknetServerConstants.UpgradeCacheMult; + let totalCost = 0; + let currentCache = startingCache; + for (let i = 0; i < sanitizedLevels; ++i) { + totalCost += Math.pow(mult, currentCache - 1); + ++currentCache; + } + totalCost *= HacknetServerConstants.CacheBaseCost; + + return totalCost; +} + +// TODO: reverse engineer hashUpgradeCost + +export function calculateServerCost(n: number, mult = 1): number { + if (n - 1 >= HacknetServerConstants.MaxServers) { + return Infinity; + } + + return HacknetServerConstants.BaseCost * Math.pow(HacknetServerConstants.PurchaseMult, n - 1) * mult; +} diff --git a/src/utils/formulas/player.ts b/src/utils/formulas/player.ts new file mode 100644 index 0000000..a1e7a97 --- /dev/null +++ b/src/utils/formulas/player.ts @@ -0,0 +1,492 @@ +import { + BitNodeOptions, + Bladeburner, + CityName, + CompanyName, + Corporation, + FactionName, + Gang, + HP, + Person as IPerson, + Player as IPlayer, + JobName, + LocationName, + Skills, + Sleeve, + WorkStats, +} from "@ns"; +import { CONSTANTS } from "./constants"; +import { currentNodeMults, PartialRecord } from "./exports"; +import { calculateSkill } from "./skills"; + +export interface Multipliers { + hacking_chance: number; + hacking_speed: number; + hacking_money: number; + hacking_grow: number; + hacking: number; + hacking_exp: number; + strength: number; + strength_exp: number; + defense: number; + defense_exp: number; + dexterity: number; + dexterity_exp: number; + agility: number; + agility_exp: number; + charisma: number; + charisma_exp: number; + hacknet_node_money: number; + hacknet_node_purchase_cost: number; + hacknet_node_ram_cost: number; + hacknet_node_core_cost: number; + hacknet_node_level_cost: number; + company_rep: number; + faction_rep: number; + work_money: number; + crime_success: number; + crime_money: number; + bladeburner_max_stamina: number; + bladeburner_stamina_gain: number; + bladeburner_analysis: number; + bladeburner_success_chance: number; +} + +export const defaultMultipliers = (): Multipliers => { + return { + hacking_chance: 1, + hacking_speed: 1, + hacking_money: 1, + hacking_grow: 1, + hacking: 1, + hacking_exp: 1, + strength: 1, + strength_exp: 1, + defense: 1, + defense_exp: 1, + dexterity: 1, + dexterity_exp: 1, + agility: 1, + agility_exp: 1, + charisma: 1, + charisma_exp: 1, + hacknet_node_money: 1, + hacknet_node_purchase_cost: 1, + hacknet_node_ram_cost: 1, + hacknet_node_core_cost: 1, + hacknet_node_level_cost: 1, + company_rep: 1, + faction_rep: 1, + work_money: 1, + crime_success: 1, + crime_money: 1, + bladeburner_max_stamina: 1, + bladeburner_stamina_gain: 1, + bladeburner_analysis: 1, + bladeburner_success_chance: 1, + }; +}; + +export let Player: PlayerObject; + +// Base class representing a person-like object +export abstract class Person implements IPerson { + hp: HP = { current: 10, max: 10 }; + skills: Skills = { + hacking: 1, + strength: 1, + defense: 1, + dexterity: 1, + agility: 1, + charisma: 1, + intelligence: 0, + }; + exp: Skills = { + hacking: 0, + strength: 0, + defense: 0, + dexterity: 0, + agility: 0, + charisma: 0, + intelligence: 0, + }; + + mults = defaultMultipliers(); + + /** Augmentations */ + // augmentations: PlayerOwnedAugmentation[] = []; + // queuedAugmentations: PlayerOwnedAugmentation[] = []; + + /** City that the person is in */ + city: CityName = CityName.Sector12; + + gainHackingExp(exp: number): void { + if (isNaN(exp)) { + console.error("ERR: NaN passed into Player.gainHackingExp()"); + return; + } + this.exp.hacking += exp; + if (this.exp.hacking < 0) { + this.exp.hacking = 0; + } + + this.skills.hacking = calculateSkill( + this.exp.hacking, + this.mults.hacking * currentNodeMults.HackingLevelMultiplier, + ); + } + + gainStrengthExp(exp: number): void { + if (isNaN(exp)) { + console.error("ERR: NaN passed into Player.gainStrengthExp()"); + return; + } + this.exp.strength += exp; + if (this.exp.strength < 0) { + this.exp.strength = 0; + } + + this.skills.strength = calculateSkill( + this.exp.strength, + this.mults.strength * currentNodeMults.StrengthLevelMultiplier, + ); + } + + gainDefenseExp(exp: number): void { + if (isNaN(exp)) { + console.error("ERR: NaN passed into player.gainDefenseExp()"); + return; + } + this.exp.defense += exp; + if (this.exp.defense < 0) { + this.exp.defense = 0; + } + + this.skills.defense = calculateSkill( + this.exp.defense, + this.mults.defense * currentNodeMults.DefenseLevelMultiplier, + ); + const ratio = this.hp.current / this.hp.max; + this.hp.max = Math.floor(10 + this.skills.defense / 10); + this.hp.current = Math.round(this.hp.max * ratio); + } + + gainDexterityExp(exp: number): void { + if (isNaN(exp)) { + console.error("ERR: NaN passed into Player.gainDexterityExp()"); + return; + } + this.exp.dexterity += exp; + if (this.exp.dexterity < 0) { + this.exp.dexterity = 0; + } + + this.skills.dexterity = calculateSkill( + this.exp.dexterity, + this.mults.dexterity * currentNodeMults.DexterityLevelMultiplier, + ); + } + + gainAgilityExp(exp: number): void { + if (isNaN(exp)) { + console.error("ERR: NaN passed into Player.gainAgilityExp()"); + return; + } + this.exp.agility += exp; + if (this.exp.agility < 0) { + this.exp.agility = 0; + } + + this.skills.agility = calculateSkill( + this.exp.agility, + this.mults.agility * currentNodeMults.AgilityLevelMultiplier, + ); + } + + gainCharismaExp(exp: number): void { + if (isNaN(exp)) { + console.error("ERR: NaN passed into Player.gainCharismaExp()"); + return; + } + this.exp.charisma += exp; + if (this.exp.charisma < 0) { + this.exp.charisma = 0; + } + + this.skills.charisma = calculateSkill( + this.exp.charisma, + this.mults.charisma * currentNodeMults.CharismaLevelMultiplier, + ); + } + + // gainIntelligenceExp(exp: number): void { + // if (isNaN(exp)) { + // console.error("ERROR: NaN passed into Player.gainIntelligenceExp()"); + // return; + // } + // /** + // * Don't change sourceFileLvl to activeSourceFileLvl. When the player has int level, the ability to gain more int is + // * a permanent benefit. + // */ + // if (Player.sourceFileLvl(5) > 0 || this.skills.intelligence > 0 || Player.bitNodeN === 5) { + // this.exp.intelligence += exp; + // this.skills.intelligence = Math.floor(this.calculateSkill(this.exp.intelligence, 1)); + // } + // } + + gainStats(retValue: WorkStats): void { + this.gainHackingExp(retValue.hackExp * this.mults.hacking_exp); + this.gainStrengthExp(retValue.strExp * this.mults.strength_exp); + this.gainDefenseExp(retValue.defExp * this.mults.defense_exp); + this.gainDexterityExp(retValue.dexExp * this.mults.dexterity_exp); + this.gainAgilityExp(retValue.agiExp * this.mults.agility_exp); + this.gainCharismaExp(retValue.chaExp * this.mults.charisma_exp); + // this.gainIntelligenceExp(retValue.intExp); + } + + regenerateHp(amt: number): void { + if (typeof amt !== "number") { + console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`); + return; + } + this.hp.current += amt; + if (this.hp.current > this.hp.max) { + this.hp.current = this.hp.max; + } + } + + updateSkillLevels(this: Person): void { + for (const [skill, bnMult] of [ + ["hacking", "HackingLevelMultiplier"], + ["strength", "StrengthLevelMultiplier"], + ["defense", "DefenseLevelMultiplier"], + ["dexterity", "DexterityLevelMultiplier"], + ["agility", "AgilityLevelMultiplier"], + ["charisma", "CharismaLevelMultiplier"], + ] as const) { + this.skills[skill] = Math.max( + 1, + Math.floor(this.calculateSkill(this.exp[skill], this.mults[skill] * currentNodeMults[bnMult])), + ); + } + + const ratio: number = Math.min(this.hp.current / this.hp.max, 1); + this.hp.max = Math.floor(10 + this.skills.defense / 10); + this.hp.current = Math.round(this.hp.max * ratio); + } + + // hasAugmentation(augName: string, ignoreQueued = false) { + // if (this.augmentations.some((a) => a.name === augName)) { + // return true; + // } + // if (!ignoreQueued && this.queuedAugmentations.some((a) => a.name === augName)) { + // return true; + // } + // return false; + // } + + // travel(cityName: CityName): boolean { + // if (!Player.canAfford(CONSTANTS.TravelCost)) { + // return false; + // } + + // Player.loseMoney(CONSTANTS.TravelCost, this.travelCostMoneySource()); + // this.city = cityName; + + // return true; + // } + + calculateSkill = calculateSkill; //Class version is equal to imported version + + /** Reset all multipliers to 1 */ + resetMultipliers() { + this.mults = defaultMultipliers(); + } + + // abstract travelCostMoneySource(): MoneySource; + // abstract takeDamage(amt: number): boolean; + // abstract whoAmI(): string; + // abstract toJSON(): IReviverValue; +} + +export class PlayerObject extends Person implements IPlayer { + // Player-specific properties + bitNodeN = 1; //current bitnode + corporation: Corporation | null = null; + gang: Gang | null = null; + bladeburner: Bladeburner | null = null; + currentServer = ""; + factions: FactionName[] = []; + factionInvitations: FactionName[] = []; + // factionRumors = new JSONSet(); + // hacknetNodes: (HacknetNode | string)[] = []; // HacknetNode object or hostname of Hacknet Server + has4SData = false; + has4SDataTixApi = false; + // hashManager = new HashManager(); + hasTixApiAccess = false; + hasWseAccount = false; + jobs: PartialRecord = {}; + karma = 0; + numPeopleKilled = 0; + location = LocationName.TravelAgency; + money = 1000 + CONSTANTS.Donations; + // moneySourceA = new MoneySourceTracker(); + // moneySourceB = new MoneySourceTracker(); + playtimeSinceLastAug = 0; + playtimeSinceLastBitnode = 0; + lastAugReset = -1; + lastNodeReset = -1; + purchasedServers: string[] = []; + scriptProdSinceLastAug = 0; + sleeves: Sleeve[] = []; + sleevesFromCovenant = 0; + // sourceFiles = new JSONMap(); + // exploits: Exploit[] = []; + // achievements: PlayerAchievement[] = []; + terminalCommandHistory: string[] = []; + // identifier: string; + lastUpdate = 0; + lastSave = 0; + totalPlaytime = 0; + + // currentWork: Work | null = null; + focus = false; + + entropy = 0; + + bitNodeOptions: BitNodeOptions = { + sourceFileOverrides: new Map(), + // sourceFileOverrides: new JSONMap(), + intelligenceOverride: undefined, + restrictHomePCUpgrade: false, + disableGang: false, + disableCorporation: false, + disableBladeburner: false, + disable4SData: false, + disableHacknetServer: false, + disableSleeveExpAndAugmentation: false, + }; + + // get activeSourceFiles(): JSONMap { + // return new JSONMap([...this.sourceFiles, ...this.bitNodeOptions.sourceFileOverrides]); + // } + + // Player-specific methods + // init = generalMethods.init; + // startWork = workMethods.startWork; + // processWork = workMethods.processWork; + // finishWork = workMethods.finishWork; + // applyForJob = generalMethods.applyForJob; + // canAccessBladeburner = bladeburnerMethods.canAccessBladeburner; + // canAccessCorporation = corporationMethods.canAccessCorporation; + // canAccessGang = gangMethods.canAccessGang; + // canAccessGrafting = generalMethods.canAccessGrafting; + // canAfford = generalMethods.canAfford; + // gainMoney = generalMethods.gainMoney; + // getCurrentServer = serverMethods.getCurrentServer; + // getGangFaction = gangMethods.getGangFaction; + // getGangName = gangMethods.getGangName; + // getHomeComputer = serverMethods.getHomeComputer; + // getNextCompanyPosition = generalMethods.getNextCompanyPosition; + // getUpgradeHomeRamCost = serverMethods.getUpgradeHomeRamCost; + // getUpgradeHomeCoresCost = serverMethods.getUpgradeHomeCoresCost; + // gotoLocation = generalMethods.gotoLocation; + // hasGangWith = gangMethods.hasGangWith; + // hasTorRouter = serverMethods.hasTorRouter; + // hasProgram = generalMethods.hasProgram; + // inGang = gangMethods.inGang; + // isAwareOfGang = gangMethods.isAwareOfGang; + // isQualified = generalMethods.isQualified; + // loseMoney = generalMethods.loseMoney; + // reapplyAllAugmentations = generalMethods.reapplyAllAugmentations; + // reapplyAllSourceFiles = generalMethods.reapplyAllSourceFiles; + // recordMoneySource = generalMethods.recordMoneySource; + // setMoney = generalMethods.setMoney; + // startBladeburner = bladeburnerMethods.startBladeburner; + // startCorporation = corporationMethods.startCorporation; + // startFocusing = generalMethods.startFocusing; + // startGang = gangMethods.startGang; + // takeDamage = generalMethods.takeDamage; + // giveExploit = generalMethods.giveExploit; + // giveAchievement = generalMethods.giveAchievement; + // getCasinoWinnings = generalMethods.getCasinoWinnings; + // quitJob = generalMethods.quitJob; + // hasJob = generalMethods.hasJob; + // createHacknetServer = serverMethods.createHacknetServer; + // queueAugmentation = generalMethods.queueAugmentation; + // receiveInvite = generalMethods.receiveInvite; + // receiveRumor = generalMethods.receiveRumor; + // gainCodingContractReward = generalMethods.gainCodingContractReward; + // stopFocusing = generalMethods.stopFocusing; + // prestigeAugmentation = generalMethods.prestigeAugmentation; + // prestigeSourceFile = generalMethods.prestigeSourceFile; + // calculateSkillProgress = generalMethods.calculateSkillProgress; + // hospitalize = generalMethods.hospitalize; + // checkForFactionInvitations = generalMethods.checkForFactionInvitations; + // setBitNodeNumber = generalMethods.setBitNodeNumber; + // canAccessCotMG = generalMethods.canAccessCotMG; + // sourceFileLvl = generalMethods.sourceFileLvl; + // activeSourceFileLvl = generalMethods.activeSourceFileLvl; + // applyEntropy = augmentationMethods.applyEntropy; + // focusPenalty = generalMethods.focusPenalty; + + // constructor() { + // super(); + // // Let's get a hash of some semi-random stuff so we have something unique. + // this.identifier = cyrb53( + // "I-" + + // new Date().getTime() + + // navigator.userAgent + + // window.innerWidth + + // window.innerHeight + + // getRandomIntInclusive(100, 999), + // ); + // this.lastAugReset = this.lastNodeReset = Date.now(); + // } + + // travelCostMoneySource(): MoneySource { + // return "other"; + // } + + // whoAmI(): string { + // return "Player"; + // } + + // sleevesSupportingBladeburner(): Sleeve[] { + // return this.sleeves.filter((s) => isSleeveSupportWork(s.currentWork)); + // } + + // /** Serialize the current object to a JSON save state. */ + // toJSON(): IReviverValue { + // return Generic_toJSON("PlayerObject", this); + // } + + /** Initializes a PlayerObject object from a JSON save state. */ + // static fromJSON(value: IReviverValue): PlayerObject { + // const player = Generic_fromJSON(PlayerObject, value.data); + // // Any statistics that could be infinite would be serialized as null (JSON.stringify(Infinity) is "null") + // player.hp = { current: player.hp?.current ?? 10, max: player.hp?.max ?? 10 }; + // player.money ??= 0; + // // Just remove from the save file any augs that have invalid name + // player.augmentations = player.augmentations.filter((ownedAug) => isMember("AugmentationName", ownedAug.name)); + // player.queuedAugmentations = player.queuedAugmentations.filter((ownedAug) => + // isMember("AugmentationName", ownedAug.name), + // ); + // player.updateSkillLevels(); + // // Conversion code for Player.sourceFiles is here instead of normal save conversion area because it needs + // // to happen earlier for use in the savegame comparison tool. + // if (Array.isArray(player.sourceFiles)) { + // // Expect pre-2.3 sourcefile format here. + // type OldSourceFiles = { n: number; lvl: number }[]; + // player.sourceFiles = new JSONMap((player.sourceFiles as OldSourceFiles).map(({ n, lvl }) => [n, lvl])); + // } + // // Remove any invalid jobs + // for (const [loadedCompanyName, loadedJobName] of Object.entries(player.jobs)) { + // if (!isMember("CompanyName", loadedCompanyName) || !isMember("JobName", loadedJobName)) { + // // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + // delete player.jobs[loadedCompanyName as CompanyName]; + // } + // } + // return player; + // } +} diff --git a/src/utils/formulas/reputation.ts b/src/utils/formulas/reputation.ts new file mode 100644 index 0000000..0e32f34 --- /dev/null +++ b/src/utils/formulas/reputation.ts @@ -0,0 +1,40 @@ +import { Person as IPerson } from "@ns"; +import { currentNodeMults } from "./exports"; +import { clampNumber } from "./utils"; +import { CONSTANTS } from "./constants"; +import { Player } from "./player"; + +export const MaxFavor = 35331; +// This is the nearest representable value of log(1.02), which is the base of our power. +// It is *not* the same as Math.log(1.02), since "1.02" lacks sufficient precision. +const log1point02 = 0.019802627296179712; + +export function favorToRep(f: number): number { + // expm1 is e^x - 1, which is more accurate for small x than doing it the obvious way. + return clampNumber(25000 * Math.expm1(log1point02 * f), 0); +} + +export function repToFavor(r: number): number { + // log1p is log(x + 1), which is more accurate for small x than doing it the obvious way. + return clampNumber(Math.log1p(r / 25000) / log1point02, 0, MaxFavor); +} + +export function calculateFavorAfterResetting(favor: number, playerReputation: number) { + return repToFavor(favorToRep(favor) + playerReputation); +} + +export function repFromDonation(amt: number, person: IPerson): number { + return (amt / CONSTANTS.DonateMoneyToRepDivisor) * person.mults.faction_rep * currentNodeMults.FactionWorkRepGain; +} + +export function donationForRep(rep: number, person: IPerson): number { + return (rep * CONSTANTS.DonateMoneyToRepDivisor) / person.mults.faction_rep / currentNodeMults.FactionWorkRepGain; +} + +export function repNeededToDonate(): number { + return Math.floor(CONSTANTS.BaseFavorToDonate * currentNodeMults.RepToDonateToFaction); +} + +export function canDonate(amt: number): boolean { + return !isNaN(amt) && amt > 0 && Player.money >= amt; +} diff --git a/src/utils/formulas/skills.ts b/src/utils/formulas/skills.ts new file mode 100644 index 0000000..b5ad82c --- /dev/null +++ b/src/utils/formulas/skills.ts @@ -0,0 +1,85 @@ +import { clampNumber } from "./utils"; + +/** + * Given an experience amount and stat multiplier, calculates the + * stat level. Stat-agnostic (same formula for every stat) + */ +export function calculateSkill(exp: number, mult = 1): number { + // Mult can be 0 in BN12 when the player has a very high SF12 level. In this case, the skill level will never change + // from its initial value (1 for most stats, except intelligence). + if (mult === 0) { + return 1; + } + const value = Math.floor(mult * (32 * Math.log(exp + 534.6) - 200)); + return clampNumber(value, 1); +} + +export function calculateExp(skill: number, mult = 1): number { + const floorSkill = Math.floor(skill); + let value = Math.exp((skill / mult + 200) / 32) - 534.6; + if (skill === floorSkill && Number.isFinite(skill) && Number.isFinite(value)) { + // Check for floating point rounding issues that would cause the inverse + // operation to return the wrong result. + let calcSkill = calculateSkill(value, mult); + let diff = Math.abs(value * Number.EPSILON); + let newValue = value; + while (calcSkill < skill) { + newValue = value + diff; + diff *= 2; + calcSkill = calculateSkill(newValue, mult); + } + value = newValue; + } + return clampNumber(value, 0); +} + +export function calculateSkillProgress(exp: number, mult = 1): ISkillProgress { + const currentSkill = calculateSkill(exp, mult); + const nextSkill = currentSkill + 1; + + const baseExperience = calculateExp(currentSkill, mult); + const nextExperience = calculateExp(nextSkill, mult); + + const normalize = (value: number): number => ((value - baseExperience) * 100) / (nextExperience - baseExperience); + + const rawProgress = nextExperience - baseExperience !== 0 ? normalize(exp) : 99.99; + const progress = clampNumber(rawProgress, 0, 100); + + const currentExperience = clampNumber(exp - baseExperience, 0); + const remainingExperience = clampNumber(nextExperience - exp, 0); + + return { + currentSkill, + nextSkill, + baseExperience, + experience: exp, + nextExperience, + currentExperience, + remainingExperience, + progress, + }; +} + +export interface ISkillProgress { + currentSkill: number; + nextSkill: number; + baseExperience: number; + experience: number; + nextExperience: number; + currentExperience: number; + remainingExperience: number; + progress: number; +} + +export function getEmptySkillProgress(): ISkillProgress { + return { + currentSkill: 0, + nextSkill: 0, + baseExperience: 0, + experience: 0, + nextExperience: 0, + currentExperience: 0, + remainingExperience: 0, + progress: 0, + }; +} diff --git a/src/utils/formulas/utils.ts b/src/utils/formulas/utils.ts new file mode 100644 index 0000000..3d394db --- /dev/null +++ b/src/utils/formulas/utils.ts @@ -0,0 +1,25 @@ +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 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.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/tsconfig.json b/tsconfig.json index 07a8384..831c90d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,9 +14,8 @@ "noEmit": true, "isolatedModules": true, "esModuleInterop": true, - "baseUrl": ".", "inlineSourceMap": true, - "moduleResolution": "Node", + "moduleResolution": "node", "resolveJsonModule": true, "skipLibCheck": true, "paths": {