diff --git a/src/Company/Companies.ts b/src/Company/Companies.ts index 93ba18479..f8666c552 100644 --- a/src/Company/Companies.ts +++ b/src/Company/Companies.ts @@ -6,6 +6,7 @@ import { assertLoadingType } from "../utils/TypeAssertion"; import { CompanyName } from "./Enums"; import { PartialRecord, createEnumKeyedRecord } from "../Types/Record"; import { getEnumHelper } from "../utils/EnumHelper"; +import { clampNumber } from "../utils/helpers/clampNumber"; export const Companies: Record = (() => { const metadata = getCompaniesMetadata(); @@ -27,8 +28,14 @@ export function loadCompanies(saveString: string): void { const company = Companies[loadedCompanyName]; assertLoadingType(loadedCompany); const { playerReputation: loadedRep, favor: loadedFavor } = loadedCompany; - if (typeof loadedRep === "number" && loadedRep > 0) company.playerReputation = loadedRep; - if (typeof loadedFavor === "number" && loadedFavor > 0) company.favor = loadedFavor; + if (typeof loadedRep === "number" && loadedRep >= 0) { + // `playerReputation` must be in [0, Number.MAX_VALUE]. + company.playerReputation = clampNumber(loadedRep, 0); + } + if (typeof loadedFavor === "number" && loadedFavor >= 0) { + // `favor` must be in [0, MaxFavor]. This rule will be enforced in the `setFavor` function. + company.setFavor(loadedFavor); + } } } diff --git a/src/Company/Company.ts b/src/Company/Company.ts index 93247189f..be2ed5785 100644 --- a/src/Company/Company.ts +++ b/src/Company/Company.ts @@ -1,7 +1,8 @@ import type { CompanyPosition } from "./CompanyPosition"; import { CompanyName, JobName, FactionName } from "@enums"; -import { favorToRep, repToFavor } from "../Faction/formulas/favor"; +import { MaxFavor, calculateFavorAfterResetting } from "../Faction/formulas/favor"; +import { clampNumber } from "../utils/helpers/clampNumber"; export interface CompanyCtorParams { name: CompanyName; @@ -37,7 +38,8 @@ export class Company { // Dynamic info, loaded from save and updated during game. playerReputation = 0; - favor = 0; + + #favor = 0; constructor(p: CompanyCtorParams) { this.name = p.name; @@ -49,26 +51,36 @@ export class Company { if (p.relatedFaction) this.relatedFaction = p.relatedFaction; } + get favor() { + return this.#favor; + } + + /** + * There is no setter for this.#favor. This is intentional. Performing arithmetic operations on `favor` may lead to + * the overflow error of `playerReputation`, so anything that wants to change `favor` must explicitly do that through + * `setFavor`. + * + * @param value + */ + setFavor(value: number) { + if (Number.isNaN(value)) { + this.#favor = 0; + return; + } + this.#favor = clampNumber(value, 0, MaxFavor); + } + hasPosition(pos: CompanyPosition | JobName): boolean { return this.companyPositions.has(typeof pos === "string" ? pos : pos.name); } prestigeAugmentation(): void { - if (this.favor == null) this.favor = 0; - this.favor += this.getFavorGain(); + this.setFavor(calculateFavorAfterResetting(this.favor, this.playerReputation)); this.playerReputation = 0; } prestigeSourceFile() { - this.favor = 0; + this.setFavor(0); this.playerReputation = 0; } - - getFavorGain(): number { - if (this.favor == null) this.favor = 0; - const storedRep = Math.max(0, favorToRep(this.favor)); - const totalRep = storedRep + this.playerReputation; - const newFavor = repToFavor(totalRep); - return newFavor - this.favor; - } } diff --git a/src/DevMenu/ui/CompaniesDev.tsx b/src/DevMenu/ui/CompaniesDev.tsx index b233bed33..dd7fe98d6 100644 --- a/src/DevMenu/ui/CompaniesDev.tsx +++ b/src/DevMenu/ui/CompaniesDev.tsx @@ -14,8 +14,9 @@ import { Companies } from "../../Company/Companies"; import { Adjuster } from "./Adjuster"; import { isMember } from "../../utils/EnumHelper"; import { getRecordValues } from "../../Types/Record"; +import { MaxFavor } from "../../Faction/formulas/favor"; -const bigNumber = 1e12; +const largeAmountOfReputation = 1e12; export function CompaniesDev(): React.ReactElement { const [companyName, setCompanyName] = useState(CompanyName.ECorp); @@ -40,18 +41,18 @@ export function CompaniesDev(): React.ReactElement { return function (favor: number): void { const company = Companies[companyName]; if (!isNaN(favor)) { - company.favor += favor * modifier; + company.setFavor(company.favor + favor * modifier); } }; } function resetCompanyFavor(): void { - Companies[companyName].favor = 0; + Companies[companyName].setFavor(0); } function tonsOfRepCompanies(): void { for (const company of getRecordValues(Companies)) { - company.playerReputation = bigNumber; + company.playerReputation = largeAmountOfReputation; } } @@ -63,13 +64,13 @@ export function CompaniesDev(): React.ReactElement { function tonsOfFavorCompanies(): void { for (const company of getRecordValues(Companies)) { - company.favor = bigNumber; + company.setFavor(MaxFavor); } } function resetAllFavorCompanies(): void { for (const company of getRecordValues(Companies)) { - company.favor = 0; + company.setFavor(0); } } @@ -103,7 +104,7 @@ export function CompaniesDev(): React.ReactElement { modifyCompanyRep(1)(bigNumber)} + tons={() => modifyCompanyRep(1)(largeAmountOfReputation)} add={modifyCompanyRep(1)} subtract={modifyCompanyRep(-1)} reset={resetCompanyRep} diff --git a/src/DevMenu/ui/FactionsDev.tsx b/src/DevMenu/ui/FactionsDev.tsx index 4baaeda53..55aa5c443 100644 --- a/src/DevMenu/ui/FactionsDev.tsx +++ b/src/DevMenu/ui/FactionsDev.tsx @@ -29,8 +29,9 @@ import { Factions } from "../../Faction/Factions"; import { getRecordValues } from "../../Types/Record"; import { getEnumHelper } from "../../utils/EnumHelper"; import { useRerender } from "../../ui/React/hooks"; +import { MaxFavor } from "../../Faction/formulas/favor"; -const bigNumber = 1e12; +const largeAmountOfReputation = 1e12; export function FactionsDev(): React.ReactElement { const [selectedFaction, setSelectedFaction] = useState(Factions[FactionName.Illuminati]); @@ -73,7 +74,9 @@ export function FactionsDev(): React.ReactElement { function modifyFactionRep(modifier: number): (x: number) => void { return function (reputation: number): void { - if (!isNaN(reputation)) selectedFaction.playerReputation += reputation * modifier; + if (!isNaN(reputation)) { + selectedFaction.playerReputation += reputation * modifier; + } }; } @@ -83,17 +86,19 @@ export function FactionsDev(): React.ReactElement { function modifyFactionFavor(modifier: number): (x: number) => void { return function (favor: number): void { - if (!isNaN(favor)) selectedFaction.favor += favor * modifier; + if (!isNaN(favor)) { + selectedFaction.setFavor(selectedFaction.favor + favor * modifier); + } }; } function resetFactionFavor(): void { - selectedFaction.favor = 0; + selectedFaction.setFavor(0); } function tonsOfRep(): void { for (const faction of getRecordValues(Factions)) { - faction.playerReputation = bigNumber; + faction.playerReputation = largeAmountOfReputation; } } @@ -105,17 +110,17 @@ export function FactionsDev(): React.ReactElement { function tonsOfFactionFavor(): void { for (const faction of getRecordValues(Factions)) { - faction.favor = bigNumber; + faction.setFavor(MaxFavor); } } function resetAllFactionFavor(): void { for (const faction of getRecordValues(Factions)) { - faction.favor = 0; + faction.setFavor(0); } } - function setDiscovery(event: React.ChangeEvent, value: string): void { + function setDiscovery(_: React.ChangeEvent, value: string): void { if (!getEnumHelper("FactionDiscovery").isMember(value)) return; selectedFaction.discovery = value; rerender(); @@ -187,7 +192,7 @@ export function FactionsDev(): React.ReactElement { modifyFactionRep(1)(bigNumber)} + tons={() => modifyFactionRep(1)(largeAmountOfReputation)} add={modifyFactionRep(1)} subtract={modifyFactionRep(-1)} reset={resetFactionRep} @@ -202,7 +207,7 @@ export function FactionsDev(): React.ReactElement { modifyFactionFavor(1)(2000)} + tons={() => modifyFactionFavor(1)(MaxFavor)} add={modifyFactionFavor(1)} subtract={modifyFactionFavor(-1)} reset={resetFactionFavor} diff --git a/src/ExportBonus.tsx b/src/ExportBonus.tsx index e00ea67c2..e84e8b36a 100644 --- a/src/ExportBonus.tsx +++ b/src/ExportBonus.tsx @@ -12,7 +12,7 @@ export function canGetBonus(): boolean { export function onExport(): void { if (!canGetBonus()) return; for (const facName of Player.factions) { - Factions[facName].favor++; + Factions[facName].setFavor(Factions[facName].favor + 1); } LastExportBonus = new Date().getTime(); } diff --git a/src/Faction/Faction.ts b/src/Faction/Faction.ts index f6d959c7e..55479f69f 100644 --- a/src/Faction/Faction.ts +++ b/src/Faction/Faction.ts @@ -1,6 +1,7 @@ import { AugmentationName, FactionName, FactionDiscovery } from "@enums"; import { FactionInfo, FactionInfos } from "./FactionInfo"; -import { favorToRep, repToFavor } from "./formulas/favor"; +import { MaxFavor, calculateFavorAfterResetting } from "./formulas/favor"; +import { clampNumber } from "../utils/helpers/clampNumber"; export class Faction { /** @@ -13,7 +14,7 @@ export class Faction { augmentations: AugmentationName[] = []; /** Amount of favor the player has with this faction. */ - favor = 0; + #favor = 0; /** Flag signalling whether player has been banned from this faction */ isBanned = false; @@ -34,6 +35,25 @@ export class Faction { this.name = name; } + get favor() { + return this.#favor; + } + + /** + * There is no setter for this.#favor. This is intentional. Performing arithmetic operations on `favor` may lead to + * the overflow error of `playerReputation`, so anything that wants to change `favor` must explicitly do that through + * `setFavor`. + * + * @param value + */ + setFavor(value: number) { + if (Number.isNaN(value)) { + this.#favor = 0; + return; + } + this.#favor = clampNumber(value, 0, MaxFavor); + } + getInfo(): FactionInfo { const info = FactionInfos[this.name]; if (info == null) { @@ -47,7 +67,7 @@ export class Faction { prestigeSourceFile() { // Reset favor, reputation, and flags - this.favor = 0; + this.setFavor(0); this.playerReputation = 0; this.alreadyInvited = false; this.isMember = false; @@ -56,23 +76,11 @@ export class Faction { prestigeAugmentation(): void { // Gain favor - if (this.favor == null) this.favor = 0; - this.favor += this.getFavorGain(); + this.setFavor(calculateFavorAfterResetting(this.favor, this.playerReputation)); // Reset reputation and flags this.playerReputation = 0; this.alreadyInvited = false; this.isMember = false; this.isBanned = false; } - - //Returns an array with [How much favor would be gained, how much rep would be left over] - getFavorGain(): number { - if (this.favor == null) { - this.favor = 0; - } - const storedRep = Math.max(0, favorToRep(this.favor)); - const totalRep = storedRep + this.playerReputation; - const newFavor = repToFavor(totalRep); - return newFavor - this.favor; - } } diff --git a/src/Faction/Factions.ts b/src/Faction/Factions.ts index d1f4cb3e7..1994e994c 100644 --- a/src/Faction/Factions.ts +++ b/src/Faction/Factions.ts @@ -8,6 +8,7 @@ import { assertLoadingType } from "../utils/TypeAssertion"; import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record"; import { Augmentations } from "../Augmentation/Augmentations"; import { getEnumHelper } from "../utils/EnumHelper"; +import { clampNumber } from "../utils/helpers/clampNumber"; /** The static list of all factions. Initialized once and never modified. */ export const Factions = createEnumKeyedRecord(FactionName, (name) => new Faction(name)); @@ -33,8 +34,14 @@ export function loadFactions(saveString: string, player: PlayerObject): void { if (typeof loadedFaction !== "object") continue; assertLoadingType(loadedFaction); const { playerReputation: loadedRep, favor: loadedFavor, discovery: loadedDiscovery } = loadedFaction; - if (typeof loadedRep === "number" && loadedRep > 0) faction.playerReputation = loadedRep; - if (typeof loadedFavor === "number" && loadedFavor > 0) faction.favor = loadedFavor; + if (typeof loadedRep === "number" && loadedRep >= 0) { + // `playerReputation` must be in [0, Number.MAX_VALUE]. + faction.playerReputation = clampNumber(loadedRep, 0); + } + if (typeof loadedFavor === "number" && loadedFavor >= 0) { + // `favor` must be in [0, MaxFavor]. This rule will be enforced in the `setFavor` function. + faction.setFavor(loadedFavor); + } if (getEnumHelper("FactionDiscovery").isMember(loadedDiscovery)) faction.discovery = loadedDiscovery; } // Load joined factions from player save diff --git a/src/Faction/formulas/favor.ts b/src/Faction/formulas/favor.ts index 73790544b..e86bb7dc3 100644 --- a/src/Faction/formulas/favor.ts +++ b/src/Faction/formulas/favor.ts @@ -2,12 +2,18 @@ // see https://en.wikipedia.org/wiki/Geometric_series#Closed-form_formula // for information on how to calculate this +import { clampNumber } from "../../utils/helpers/clampNumber"; + +export const MaxFavor = 35331; + export function favorToRep(f: number): number { - const raw = 25000 * (Math.pow(1.02, f) - 1); - return Math.round(raw * 10000) / 10000; // round to make things easier. + return clampNumber(25000 * (Math.pow(1.02, f) - 1), 0); } export function repToFavor(r: number): number { - const raw = Math.log(r / 25000 + 1) / Math.log(1.02); - return Math.round(raw * 10000) / 10000; // round to make things easier. + return clampNumber(Math.log(r / 25000 + 1) / Math.log(1.02), 0, MaxFavor); +} + +export function calculateFavorAfterResetting(favor: number, playerReputation: number) { + return repToFavor(favorToRep(favor) + playerReputation); } diff --git a/src/Faction/ui/Info.tsx b/src/Faction/ui/Info.tsx index b32d0b6bb..e8028faf4 100644 --- a/src/Faction/ui/Info.tsx +++ b/src/Faction/ui/Info.tsx @@ -17,6 +17,7 @@ import Typography from "@mui/material/Typography"; import Tooltip from "@mui/material/Tooltip"; import Box from "@mui/material/Box"; import { useRerender } from "../../ui/React/hooks"; +import { calculateFavorAfterResetting } from "../formulas/favor"; interface IProps { faction: Faction; @@ -49,8 +50,6 @@ export function Info(props: IProps): React.ReactElement { const Assignment = props.factionInfo.assignment ?? DefaultAssignment; - const favorGain = props.faction.getFavorGain(); - return ( <> {props.factionInfo.infoText} @@ -60,8 +59,9 @@ export function Info(props: IProps): React.ReactElement { title={ <> - You will have faction favor after - installing an Augmentation. + You will have{" "} + {" "} + faction favor after installing an Augmentation. {"\\(\\huge{r = \\text{total faction reputation}}\\)"} diff --git a/src/Go/boardAnalysis/scoring.ts b/src/Go/boardAnalysis/scoring.ts index 196998a6c..d75acc1d9 100644 --- a/src/Go/boardAnalysis/scoring.ts +++ b/src/Go/boardAnalysis/scoring.ts @@ -76,7 +76,7 @@ export function endGoGame(boardState: BoardState) { Player.factions.includes(factionName) && statusToUpdate.favor < getMaxFavor() ) { - Factions[factionName].favor++; + Factions[factionName].setFavor(Factions[factionName].favor + 1); statusToUpdate.favor++; } } diff --git a/src/Hacknet/HacknetHelpers.tsx b/src/Hacknet/HacknetHelpers.tsx index e4eeae02c..fa1a5335e 100644 --- a/src/Hacknet/HacknetHelpers.tsx +++ b/src/Hacknet/HacknetHelpers.tsx @@ -570,7 +570,7 @@ export function purchaseHashUpgrade(upgName: string, upgTarget: string, count = console.error(`Invalid target specified in purchaseHashUpgrade(): ${upgTarget}`); throw new Error(`'${upgTarget}' is not a company.`); } - Companies[upgTarget].favor += 5 * count; + Companies[upgTarget].setFavor(Companies[upgTarget].favor + 5 * count); break; } default: diff --git a/src/Locations/ui/CompanyLocation.tsx b/src/Locations/ui/CompanyLocation.tsx index bcfe303b9..1d69b3ff7 100644 --- a/src/Locations/ui/CompanyLocation.tsx +++ b/src/Locations/ui/CompanyLocation.tsx @@ -24,6 +24,7 @@ import { companyNameAsLocationName } from "../../Company/utils"; import { JobSummary } from "../../Company/ui/JobSummary"; import { StatsTable } from "../../ui/React/StatsTable"; import { JobListings } from "../../Company/ui/JobListings"; +import { calculateFavorAfterResetting } from "../../Faction/formulas/favor"; interface IProps { companyName: CompanyName; @@ -98,7 +99,6 @@ export function CompanyLocation(props: IProps): React.ReactElement { } const isEmployedHere = currentPosition != null; - const favorGain = company.getFavorGain(); return ( <> @@ -120,8 +120,9 @@ export function CompanyLocation(props: IProps): React.ReactElement { key="repLabel" title={ <> - You will have company favor upon resetting after - installing Augmentations + You will have{" "} + company + favor upon resetting after installing Augmentations } > diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index 4ad8ab147..15935d562 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -58,6 +58,7 @@ import { JobTracks } from "../Company/data/JobTracks"; import { ServerConstants } from "../Server/data/Constants"; import { blackOpsArray } from "../Bladeburner/data/BlackOperations"; import { calculateEffectiveRequiredReputation } from "../Company/utils"; +import { calculateFavorAfterResetting } from "../Faction/formulas/favor"; export function NetscriptSingularity(): InternalAPI { const runAfterReset = function (cbScript: ScriptFilePath) { @@ -761,7 +762,8 @@ export function NetscriptSingularity(): InternalAPI { getCompanyFavorGain: (ctx) => (_companyName) => { helpers.checkSingularityAccess(ctx); const companyName = getEnumHelper("CompanyName").nsGetMember(ctx, _companyName); - return Companies[companyName].getFavorGain(); + const company = Companies[companyName]; + return calculateFavorAfterResetting(company.favor, company.playerReputation) - company.favor; }, getFactionInviteRequirements: (ctx) => (_facName) => { helpers.checkSingularityAccess(ctx); @@ -911,7 +913,7 @@ export function NetscriptSingularity(): InternalAPI { helpers.checkSingularityAccess(ctx); const facName = getEnumHelper("FactionName").nsGetMember(ctx, _facName); const faction = Factions[facName]; - return faction.getFavorGain(); + return calculateFavorAfterResetting(faction.favor, faction.playerReputation) - faction.favor; }, donateToFaction: (ctx) => (_facName, _amt) => { helpers.checkSingularityAccess(ctx); diff --git a/test/jest/FullSave.test.ts b/test/jest/FullSave.test.ts index 82a35662f..4a0fe0a0b 100644 --- a/test/jest/FullSave.test.ts +++ b/test/jest/FullSave.test.ts @@ -57,11 +57,11 @@ function establishInitialConditions() { joinFaction(csec); joinFaction(slumSnakes); csec.playerReputation = 1e6; - csec.favor = 20; + csec.setFavor(20); // Companies const noodleBar = Companies[CompanyName.NoodleBar]; - noodleBar.favor = 100; + noodleBar.setFavor(100); noodleBar.playerReputation = 100000; // Bladeburner. Adding rank will also add bladeburner faction rep.