mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 14:28:36 +02:00
597 lines
20 KiB
TypeScript
597 lines
20 KiB
TypeScript
import {
|
|
AugmentationName,
|
|
CityName,
|
|
CompanyName,
|
|
CompletedProgramName,
|
|
FactionName,
|
|
FactionDiscovery,
|
|
JobName,
|
|
LocationName,
|
|
ToastVariant,
|
|
} from "@enums";
|
|
|
|
import type { PlayerObject } from "./PlayerObject";
|
|
import type { ProgramFilePath } from "../../Paths/ProgramFilePath";
|
|
|
|
import { applyAugmentation } from "../../Augmentation/AugmentationHelpers";
|
|
import { PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation";
|
|
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
|
|
import { CodingContractRewardType, ICodingContractReward } from "../../CodingContracts";
|
|
import { Company } from "../../Company/Company";
|
|
import { Companies } from "../../Company/Companies";
|
|
import { getNextCompanyPositionHelper } from "../../Company/GetNextCompanyPosition";
|
|
import { getJobRequirements, getJobRequirementText } from "../../Company/GetJobRequirements";
|
|
import { CompanyPosition } from "../../Company/CompanyPosition";
|
|
import { CONSTANTS } from "../../Constants";
|
|
import { Exploit } from "../../Exploits/Exploit";
|
|
import { Faction } from "../../Faction/Faction";
|
|
import { Factions } from "../../Faction/Factions";
|
|
import { FactionInvitationEvents } from "../../Faction/ui/FactionInvitationManager";
|
|
import { resetGangs } from "../../Gang/AllGangs";
|
|
import { Cities } from "../../Locations/Cities";
|
|
import { Locations } from "../../Locations/Locations";
|
|
import { Sleeve } from "../Sleeve/Sleeve";
|
|
import { SleeveWorkType } from "../Sleeve/Work/Work";
|
|
import { calculateSkillProgress as calculateSkillProgressF, ISkillProgress } from "../formulas/skill";
|
|
import { AddToAllServers, createUniqueRandomIp } from "../../Server/AllServers";
|
|
import { safelyCreateUniqueServer } from "../../Server/ServerHelpers";
|
|
|
|
import { SpecialServers } from "../../Server/data/SpecialServers";
|
|
import { applySourceFile } from "../../SourceFile/applySourceFile";
|
|
import { applyExploit } from "../../Exploits/applyExploits";
|
|
import { SourceFiles } from "../../SourceFile/SourceFiles";
|
|
import { getHospitalizationCost } from "../../Hospital/Hospital";
|
|
|
|
import { formatMoney } from "../../ui/formatNumber";
|
|
import { MoneySource, MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
|
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
|
|
|
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
|
import { achievements } from "../../Achievements/Achievements";
|
|
|
|
import { isCompanyWork } from "../../Work/CompanyWork";
|
|
import { isMember } from "../../utils/EnumHelper";
|
|
|
|
export function init(this: PlayerObject): void {
|
|
/* Initialize Player's home computer */
|
|
const t_homeComp = safelyCreateUniqueServer({
|
|
adminRights: true,
|
|
hostname: "home",
|
|
ip: createUniqueRandomIp(),
|
|
isConnectedTo: true,
|
|
maxRam: 8,
|
|
organizationName: "Home PC",
|
|
purchasedByPlayer: true,
|
|
});
|
|
this.currentServer = SpecialServers.Home;
|
|
AddToAllServers(t_homeComp);
|
|
|
|
this.getHomeComputer().programs.push(CompletedProgramName.nuke);
|
|
}
|
|
|
|
export function prestigeAugmentation(this: PlayerObject): void {
|
|
this.currentServer = SpecialServers.Home;
|
|
|
|
this.numPeopleKilled = 0;
|
|
|
|
//Reset stats
|
|
this.skills.hacking = 1;
|
|
|
|
this.skills.strength = 1;
|
|
this.skills.defense = 1;
|
|
this.skills.dexterity = 1;
|
|
this.skills.agility = 1;
|
|
|
|
this.skills.charisma = 1;
|
|
|
|
this.exp.hacking = 0;
|
|
this.exp.strength = 0;
|
|
this.exp.defense = 0;
|
|
this.exp.dexterity = 0;
|
|
this.exp.agility = 0;
|
|
this.exp.charisma = 0;
|
|
|
|
this.money = 1000 + CONSTANTS.Donations;
|
|
|
|
this.city = CityName.Sector12;
|
|
this.location = LocationName.TravelAgency;
|
|
|
|
this.jobs = {};
|
|
|
|
this.purchasedServers = [];
|
|
|
|
this.factions = [];
|
|
this.factionInvitations = [];
|
|
this.factionRumors.clear();
|
|
// Clear any pending invitation modals
|
|
FactionInvitationEvents.emit({ type: "ClearAll" });
|
|
|
|
this.queuedAugmentations = [];
|
|
|
|
Sleeve.recalculateNumOwned();
|
|
|
|
this.sleeves.forEach((sleeve) => (sleeve.shock <= 0 ? sleeve.synchronize() : sleeve.shockRecovery()));
|
|
|
|
this.lastUpdate = new Date().getTime();
|
|
|
|
// Statistics Trackers
|
|
this.playtimeSinceLastAug = 0;
|
|
this.lastAugReset = this.lastUpdate;
|
|
this.scriptProdSinceLastAug = 0;
|
|
this.moneySourceA.reset();
|
|
|
|
this.hacknetNodes.length = 0;
|
|
this.hashManager.prestige();
|
|
|
|
// Reapply augs, re-calculate skills and reset HP
|
|
this.reapplyAllAugmentations(true);
|
|
this.hp.current = this.hp.max;
|
|
|
|
this.finishWork(true, true);
|
|
}
|
|
|
|
export function prestigeSourceFile(this: PlayerObject): void {
|
|
this.entropy = 0;
|
|
this.prestigeAugmentation();
|
|
this.karma = 0;
|
|
// Duplicate sleeves are reset to level 1 every Bit Node (but the number of sleeves you have persists)
|
|
this.sleeves.forEach((sleeve) => sleeve.prestige());
|
|
|
|
if (this.bitNodeN === 10) {
|
|
for (let i = 0; i < this.sleeves.length; i++) {
|
|
this.sleeves[i].shock = Math.min(25, this.sleeves[i].shock);
|
|
this.sleeves[i].sync = Math.max(25, this.sleeves[i].sync);
|
|
}
|
|
}
|
|
|
|
this.gang = null;
|
|
resetGangs();
|
|
this.corporation = null;
|
|
this.bladeburner = null;
|
|
|
|
// Reset Stock market
|
|
this.hasWseAccount = false;
|
|
this.hasTixApiAccess = false;
|
|
this.has4SData = false;
|
|
this.has4SDataTixApi = false;
|
|
|
|
// BitNode 3: Corporatocracy
|
|
this.corporation = null;
|
|
|
|
this.moneySourceB.reset();
|
|
this.playtimeSinceLastBitnode = 0;
|
|
this.lastNodeReset = this.lastUpdate;
|
|
this.augmentations = [];
|
|
}
|
|
|
|
export function receiveInvite(this: PlayerObject, factionName: FactionName): void {
|
|
const faction = Factions[factionName];
|
|
if (this.factionInvitations.includes(factionName) || faction.alreadyInvited || faction.isMember || faction.isBanned)
|
|
return;
|
|
this.factionInvitations.push(factionName);
|
|
this.factionRumors.delete(factionName);
|
|
faction.discovery = FactionDiscovery.known;
|
|
}
|
|
|
|
export function receiveRumor(this: PlayerObject, factionName: FactionName): void {
|
|
const faction = Factions[factionName];
|
|
if (faction.discovery === FactionDiscovery.unknown) faction.discovery = FactionDiscovery.rumored;
|
|
if (this.factionRumors.has(factionName) || faction.isMember || faction.isBanned || faction.alreadyInvited) return;
|
|
this.factionRumors.add(factionName);
|
|
}
|
|
|
|
//Calculates skill level progress based on experience. The same formula will be used for every skill
|
|
export function calculateSkillProgress(this: PlayerObject, exp: number, mult = 1): ISkillProgress {
|
|
return calculateSkillProgressF(exp, mult);
|
|
}
|
|
|
|
export function hasProgram(this: PlayerObject, programName: CompletedProgramName | ProgramFilePath): boolean {
|
|
const home = this.getHomeComputer();
|
|
return home.programs.includes(programName);
|
|
}
|
|
|
|
export function setMoney(this: PlayerObject, money: number): void {
|
|
if (isNaN(money)) {
|
|
console.error("NaN passed into Player.setMoney()");
|
|
return;
|
|
}
|
|
this.money = money;
|
|
}
|
|
|
|
export function gainMoney(this: PlayerObject, money: number, source: MoneySource): void {
|
|
if (isNaN(money)) {
|
|
console.error("NaN passed into Player.gainMoney()");
|
|
return;
|
|
}
|
|
|
|
this.money = this.money + money;
|
|
this.recordMoneySource(money, source);
|
|
}
|
|
|
|
export function loseMoney(this: PlayerObject, money: number, source: MoneySource): void {
|
|
if (isNaN(money)) {
|
|
console.error("NaN passed into Player.loseMoney()");
|
|
return;
|
|
}
|
|
if (this.money === Infinity && money === Infinity) return;
|
|
this.money = this.money - money;
|
|
this.recordMoneySource(-1 * money, source);
|
|
}
|
|
|
|
export function canAfford(this: PlayerObject, cost: number): boolean {
|
|
if (isNaN(cost)) {
|
|
console.error(`NaN passed into Player.canAfford()`);
|
|
return false;
|
|
}
|
|
return this.money >= cost;
|
|
}
|
|
|
|
export function recordMoneySource(this: PlayerObject, amt: number, source: MoneySource): void {
|
|
if (!(this.moneySourceA instanceof MoneySourceTracker)) {
|
|
console.warn(`Player.moneySourceA was not properly initialized. Resetting`);
|
|
this.moneySourceA = new MoneySourceTracker();
|
|
}
|
|
if (!(this.moneySourceB instanceof MoneySourceTracker)) {
|
|
console.warn(`Player.moneySourceB was not properly initialized. Resetting`);
|
|
this.moneySourceB = new MoneySourceTracker();
|
|
}
|
|
this.moneySourceA.record(amt, source);
|
|
this.moneySourceB.record(amt, source);
|
|
}
|
|
|
|
export function startFocusing(this: PlayerObject): void {
|
|
this.focus = true;
|
|
}
|
|
|
|
export function stopFocusing(this: PlayerObject): void {
|
|
this.focus = false;
|
|
}
|
|
|
|
// Returns true if hospitalized, false otherwise
|
|
export function takeDamage(this: PlayerObject, amt: number): boolean {
|
|
if (typeof amt !== "number") {
|
|
console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`);
|
|
return false;
|
|
}
|
|
|
|
this.hp.current -= amt;
|
|
if (this.hp.current <= 0) {
|
|
this.hospitalize(false);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function hospitalize(this: PlayerObject, suppressNotification: boolean): number {
|
|
const cost = getHospitalizationCost();
|
|
this.loseMoney(cost, "hospitalization");
|
|
this.hp.current = this.hp.max;
|
|
if (!suppressNotification) {
|
|
SnackbarEvents.emit(`You've been Hospitalized for ${formatMoney(cost)}`, ToastVariant.SUCCESS, 2000);
|
|
}
|
|
return cost;
|
|
}
|
|
|
|
/**
|
|
* Company job application. Determines the job that the Player should get (if any) at the given company.
|
|
* @param this The player instance
|
|
* @param company The company being applied to
|
|
* @param position A specific position
|
|
* @param sing Whether this is being called from the applyToCompany() Netscript Singularity function
|
|
* @returns The name of the Job received (if any). May be higher or lower than the job applied to.
|
|
*/
|
|
export function applyForJob(
|
|
this: PlayerObject,
|
|
company: Company,
|
|
position: CompanyPosition,
|
|
sing = false,
|
|
): JobName | null {
|
|
if (!company) return null;
|
|
|
|
// Start searching the job track from the provided point (which may not be the entry position)
|
|
let pos = position;
|
|
if (!this.isQualified(company, pos)) {
|
|
if (!sing) {
|
|
dialogBoxCreate(`Unfortunately, you do not qualify for this position.\n${getJobRequirementText(company, pos)}`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
if (!company.hasPosition(pos)) {
|
|
console.error(`Company ${company.name} does not have position ${pos}. Player.applyToCompany() failed.`);
|
|
return null;
|
|
}
|
|
|
|
let nextPos = getNextCompanyPositionHelper(pos);
|
|
while (nextPos && company.hasPosition(nextPos) && this.isQualified(company, nextPos)) {
|
|
pos = nextPos;
|
|
nextPos = getNextCompanyPositionHelper(pos);
|
|
}
|
|
|
|
//Check if player already has the assigned job
|
|
if (this.jobs[company.name] === pos.name) {
|
|
if (!sing) {
|
|
const nextPos = getNextCompanyPositionHelper(pos);
|
|
if (nextPos == null) {
|
|
dialogBoxCreate(`You are already ${pos.name}! No promotion available`);
|
|
} else if (!company.hasPosition(nextPos)) {
|
|
dialogBoxCreate(
|
|
`You already have the highest ${pos.field} position available at ${company.name}! No promotion available`,
|
|
);
|
|
} else {
|
|
dialogBoxCreate(
|
|
`Unfortunately, you do not qualify for a promotion.\n${getJobRequirementText(company, nextPos)}`,
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
this.jobs[company.name] = pos.name;
|
|
|
|
if (!sing) {
|
|
dialogBoxCreate(`${pos.hiredText} at ${company.name}!`);
|
|
}
|
|
return pos.name;
|
|
}
|
|
|
|
/**
|
|
* Get a job position that the player can apply for.
|
|
* @param this The player instance
|
|
* @param company The Company being applied to
|
|
* @param entryPosType Job field (Software, Business, etc)
|
|
* @returns The highest job the player can apply for at this company, if any
|
|
*/
|
|
export function getNextCompanyPosition(
|
|
this: PlayerObject,
|
|
company: Company,
|
|
entryPosType: CompanyPosition,
|
|
): CompanyPosition | null {
|
|
let pos: CompanyPosition | null = entryPosType;
|
|
let nextPos = getNextCompanyPositionHelper(pos);
|
|
// Find the highest-level job in this category that the player is currently able to apply for.
|
|
while (nextPos && company.hasPosition(nextPos) && this.isQualified(company, nextPos)) {
|
|
pos = nextPos;
|
|
nextPos = getNextCompanyPositionHelper(pos);
|
|
}
|
|
// If the player already has this position, return the one after that (if any).
|
|
if (this.jobs[company.name] == pos.name) {
|
|
pos = nextPos;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
export function quitJob(this: PlayerObject, company: CompanyName, suppressDialog?: boolean): void {
|
|
if (isCompanyWork(this.currentWork) && this.currentWork.companyName === company) {
|
|
this.finishWork(true);
|
|
}
|
|
for (const sleeve of this.sleeves) {
|
|
if (sleeve.currentWork?.type === SleeveWorkType.COMPANY && sleeve.currentWork.companyName === company) {
|
|
sleeve.stopWork();
|
|
if (!suppressDialog) {
|
|
dialogBoxCreate(`You quit ${company} while one of your sleeves was working there. The sleeve is now idle.`);
|
|
}
|
|
}
|
|
}
|
|
delete this.jobs[company];
|
|
}
|
|
|
|
/**
|
|
* Method to see if the player has at least one job assigned to them
|
|
* @param this The player instance
|
|
* @returns Whether the user has at least one job
|
|
*/
|
|
export function hasJob(this: PlayerObject): boolean {
|
|
return Boolean(Object.keys(this.jobs).length);
|
|
}
|
|
|
|
//Checks if the Player is qualified for a certain position
|
|
export function isQualified(this: PlayerObject, company: Company, position: CompanyPosition): boolean {
|
|
const reqs = getJobRequirements(company, position);
|
|
return reqs.every((req) => req.isSatisfied(this));
|
|
}
|
|
|
|
/********** Reapplying Augmentations and Source File ***********/
|
|
export function reapplyAllAugmentations(this: PlayerObject, resetMultipliers = true): void {
|
|
if (resetMultipliers) {
|
|
this.resetMultipliers();
|
|
}
|
|
|
|
for (const playerAug of this.augmentations) {
|
|
const augName = playerAug.name;
|
|
|
|
if (augName == AugmentationName.NeuroFluxGovernor) {
|
|
for (let i = 0; i < playerAug.level; ++i) {
|
|
applyAugmentation(playerAug, true);
|
|
}
|
|
continue;
|
|
}
|
|
applyAugmentation(playerAug, true);
|
|
}
|
|
|
|
this.updateSkillLevels();
|
|
}
|
|
|
|
export function reapplyAllSourceFiles(this: PlayerObject): void {
|
|
//Will always be called after reapplyAllAugmentations() so multipliers do not have to be reset
|
|
//this.resetMultipliers();
|
|
|
|
for (const [bn, lvl] of this.sourceFiles) {
|
|
const srcFileKey = "SourceFile" + bn;
|
|
const sourceFileObject = SourceFiles[srcFileKey];
|
|
if (!sourceFileObject) {
|
|
console.error(`Invalid source file number: ${bn}`);
|
|
continue;
|
|
}
|
|
applySourceFile(bn, lvl);
|
|
}
|
|
applyExploit();
|
|
this.updateSkillLevels();
|
|
}
|
|
|
|
/**
|
|
* Checks whether a player meets the requirements for joining each faction, and returns an array of all invitations the player should receive.
|
|
* Also handles receiving rumors for factions if the rumor requirements are met.
|
|
*/
|
|
export function checkForFactionInvitations(this: PlayerObject): Faction[] {
|
|
const invitedFactions = [];
|
|
for (const faction of Object.values(Factions)) {
|
|
if (faction.isBanned) continue;
|
|
if (faction.isMember) continue;
|
|
if (faction.alreadyInvited) continue;
|
|
// Handle invites
|
|
const { inviteReqs, rumorReqs } = faction.getInfo();
|
|
if (inviteReqs.isSatisfied(this)) invitedFactions.push(faction);
|
|
// Handle rumors
|
|
if (this.factionRumors.has(faction.name)) continue;
|
|
if (rumorReqs.isSatisfied(this)) this.receiveRumor(faction.name);
|
|
}
|
|
return invitedFactions;
|
|
}
|
|
|
|
/************* BitNodes **************/
|
|
export function setBitNodeNumber(this: PlayerObject, n: number): void {
|
|
this.bitNodeN = n;
|
|
}
|
|
|
|
export function queueAugmentation(this: PlayerObject, name: AugmentationName): void {
|
|
for (const aug of this.queuedAugmentations) {
|
|
if (aug.name == name) {
|
|
console.warn(`tried to queue ${name} twice, this may be a bug`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (const aug of this.augmentations) {
|
|
if (aug.name == name) {
|
|
console.warn(`tried to queue ${name} twice, this may be a bug`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.queuedAugmentations.push(new PlayerOwnedAugmentation(name));
|
|
}
|
|
|
|
/************* Coding Contracts **************/
|
|
export function gainCodingContractReward(
|
|
this: PlayerObject,
|
|
reward: ICodingContractReward | null,
|
|
difficulty = 1,
|
|
): string {
|
|
if (!reward) return `No reward for this contract`;
|
|
|
|
switch (reward.type) {
|
|
case CodingContractRewardType.FactionReputation: {
|
|
if (!Factions[reward.name]) {
|
|
return this.gainCodingContractReward({ type: CodingContractRewardType.FactionReputationAll });
|
|
}
|
|
const repGain = CONSTANTS.CodingContractBaseFactionRepGain * difficulty;
|
|
Factions[reward.name].playerReputation += repGain;
|
|
return `Gained ${repGain} faction reputation for ${reward.name}`;
|
|
}
|
|
case CodingContractRewardType.FactionReputationAll: {
|
|
const totalGain = CONSTANTS.CodingContractBaseFactionRepGain * difficulty;
|
|
|
|
// Ignore Bladeburners and other special factions for this calculation
|
|
const specialFactions = [
|
|
FactionName.Bladeburners,
|
|
FactionName.ShadowsOfAnarchy,
|
|
FactionName.ChurchOfTheMachineGod,
|
|
];
|
|
const factions = this.factions.slice().filter((f) => {
|
|
return !specialFactions.includes(f);
|
|
});
|
|
|
|
// If the player was only part of the special factions, we'll just give money
|
|
if (factions.length == 0) {
|
|
return this.gainCodingContractReward({ type: CodingContractRewardType.Money }, difficulty);
|
|
}
|
|
|
|
const gainPerFaction = Math.floor(totalGain / factions.length);
|
|
for (const facName of factions) {
|
|
if (!Factions[facName]) continue;
|
|
Factions[facName].playerReputation += gainPerFaction;
|
|
}
|
|
return `Gained ${gainPerFaction} reputation for each of the following factions: ${factions.join(", ")}`;
|
|
}
|
|
case CodingContractRewardType.CompanyReputation: {
|
|
if (!isMember("CompanyName", reward.name)) {
|
|
return this.gainCodingContractReward({ type: CodingContractRewardType.FactionReputationAll });
|
|
}
|
|
const repGain = CONSTANTS.CodingContractBaseCompanyRepGain * difficulty;
|
|
Companies[reward.name].playerReputation += repGain;
|
|
return `Gained ${repGain} company reputation for ${reward.name}`;
|
|
}
|
|
case CodingContractRewardType.Money:
|
|
default: {
|
|
const moneyGain = CONSTANTS.CodingContractBaseMoneyGain * difficulty * currentNodeMults.CodingContractMoney;
|
|
this.gainMoney(moneyGain, "codingcontract");
|
|
return `Gained ${formatMoney(moneyGain)}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function travel(this: PlayerObject, cityName: CityName): boolean {
|
|
if (Cities[cityName] == null) {
|
|
throw new Error(`Player.travel() was called with an invalid city: ${cityName}`);
|
|
}
|
|
if (!this.canAfford(CONSTANTS.TravelCost)) {
|
|
return false;
|
|
}
|
|
|
|
this.loseMoney(CONSTANTS.TravelCost, "other");
|
|
this.city = cityName;
|
|
|
|
return true;
|
|
}
|
|
|
|
export function gotoLocation(this: PlayerObject, to: LocationName): boolean {
|
|
if (Locations[to] == null) {
|
|
throw new Error(`Player.gotoLocation() was called with an invalid location: ${to}`);
|
|
}
|
|
this.location = to;
|
|
|
|
return true;
|
|
}
|
|
|
|
export function canAccessGrafting(this: PlayerObject): boolean {
|
|
return this.bitNodeN === 10 || this.sourceFileLvl(10) > 0;
|
|
}
|
|
|
|
export function giveExploit(this: PlayerObject, exploit: Exploit): void {
|
|
if (!this.exploits.includes(exploit)) {
|
|
this.exploits.push(exploit);
|
|
SnackbarEvents.emit("SF -1 acquired!", ToastVariant.SUCCESS, 2000);
|
|
}
|
|
}
|
|
|
|
export function giveAchievement(this: PlayerObject, achievementId: string): void {
|
|
const achievement = achievements[achievementId];
|
|
if (!achievement) return;
|
|
if (!this.achievements.map((a) => a.ID).includes(achievementId)) {
|
|
this.achievements.push({ ID: achievementId, unlockedOn: new Date().getTime() });
|
|
SnackbarEvents.emit(`Unlocked Achievement: "${achievement.Name}"`, ToastVariant.SUCCESS, 2000);
|
|
}
|
|
}
|
|
|
|
export function getCasinoWinnings(this: PlayerObject): number {
|
|
return this.moneySourceA.casino;
|
|
}
|
|
|
|
export function canAccessCotMG(this: PlayerObject): boolean {
|
|
return this.bitNodeN === 13 || this.sourceFileLvl(13) > 0;
|
|
}
|
|
|
|
export function sourceFileLvl(this: PlayerObject, n: number): number {
|
|
return this.sourceFiles.get(n) ?? 0;
|
|
}
|
|
|
|
export function focusPenalty(this: PlayerObject): number {
|
|
let focus = 1;
|
|
if (!this.hasAugmentation(AugmentationName.NeuroreceptorManager, true)) {
|
|
focus = this.focus ? 1 : CONSTANTS.BaseFocusBonus;
|
|
}
|
|
return focus;
|
|
}
|