Merge branch 'dev' into improvement/work-in-progress-ui

This commit is contained in:
nickofolas 2022-05-04 12:28:33 -05:00
commit bcb6176952
72 changed files with 1325 additions and 820 deletions

4
dist/main.bundle.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

42
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -336,12 +336,12 @@ The list contains the name of (i.e. the value returned by
| | | | | | | |
| | | You are given an LZ-encoded string. Decode it and output the original string. | | | | You are given an LZ-encoded string. Decode it and output the original string. |
| | | | | | | |
| | | Example: decoding '5aaabc340533bca' chunk-by-chunk | | | | Example: decoding '5aaabb450723abb' chunk-by-chunk |
| | | 5aaabc -> aaabc | | | | 5aaabb -> aaabb |
| | | 5aaabc34 -> aaabcaab | | | | 5aaabb45 -> aaabbaaab |
| | | 5aaabc340 -> aaabcaab | | | | 5aaabb450 -> aaabbaaab |
| | | 5aaabc34053 -> aaabcaabaabaa | | | | 5aaabb45072 -> aaabbaaababababa |
| | | 5aaabc340533bca -> aaabcaabaabaabca | | | | 5aaabb450723abb -> aaabbaaababababaabb |
+-----------------------------------------+------------------------------------------------------------------------------------------+ +-----------------------------------------+------------------------------------------------------------------------------------------+
| Compression III: LZ Compression | | Lempel-Ziv (LZ) compression is a data compression technique which encodes data using | | Compression III: LZ Compression | | Lempel-Ziv (LZ) compression is a data compression technique which encodes data using |
| | | references to earlier parts of the data. In this variant of LZ, data is encoded in two | | | | references to earlier parts of the data. In this variant of LZ, data is encoded in two |
@ -361,12 +361,12 @@ The list contains the name of (i.e. the value returned by
| | | possible output length. | | | | possible output length. |
| | | | | | | |
| | | Examples (some have other possible encodings of minimal length): | | | | Examples (some have other possible encodings of minimal length): |
| | | abracadabra -> 7abracad47 | | | | abracadabra -> 7abracad47 |
| | | mississippi -> 4miss433ppi | | | | mississippi -> 4miss433ppi |
| | | aAAaAAaAaAA -> 3aAA53035 | | | | aAAaAAaAaAA -> 3aAA53035 |
| | | 2718281828 -> 627182844 | | | | 2718281828 -> 627182844 |
| | | abcdefghijk -> 9abcdefghi02jk | | | | abcdefghijk -> 9abcdefghi02jk |
| | | aaaaaaaaaaa -> 1a911a | | | | aaaaaaaaaaaa -> 3aaa91 |
| | | aaaaaaaaaaaa -> 1a912aa | | | | aaaaaaaaaaaaa -> 1a91031 |
| | | aaaaaaaaaaaaa -> 1a91031 | | | | aaaaaaaaaaaaaa -> 1a91041 |
+-----------------------------------------+------------------------------------------------------------------------------------------+ +-----------------------------------------+------------------------------------------------------------------------------------------+

@ -334,7 +334,7 @@
}, },
"BLADEBURNER_UNSPENT_100000": { "BLADEBURNER_UNSPENT_100000": {
"ID": "BLADEBURNER_UNSPENT_100000", "ID": "BLADEBURNER_UNSPENT_100000",
"Name": "You should really spent those.", "Name": "You should really spend those.",
"Description": "Have 100 000 unspent bladeburner skill points." "Description": "Have 100 000 unspent bladeburner skill points."
}, },
"4S": { "4S": {

@ -9,6 +9,18 @@ import { Money } from "../ui/React/Money";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver";
import { FactionNames } from "../Faction/data/FactionNames"; import { FactionNames } from "../Faction/data/FactionNames";
import { IPlayer } from "../PersonObjects/IPlayer";
import { AugmentationNames } from "./data/AugmentationNames";
import { CONSTANTS } from "../Constants";
import { StaticAugmentations } from "./StaticAugmentations";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { getBaseAugmentationPriceMultiplier, getGenericAugmentationPriceMultiplier } from "./AugmentationHelpers";
import { initSoAAugmentations } from "./data/AugmentationCreator";
export interface AugmentationCosts {
moneyCost: number;
repCost: number;
}
export interface IConstructorParams { export interface IConstructorParams {
info: string | JSX.Element; info: string | JSX.Element;
@ -410,10 +422,10 @@ function generateStatsDescription(mults: IMap<number>, programs?: string[], star
} }
export class Augmentation { export class Augmentation {
// How much money this costs to buy // How much money this costs to buy before multipliers
baseCost = 0; baseCost = 0;
// How much faction reputation is required to unlock this // How much faction reputation is required to unlock this before multipliers
baseRepRequirement = 0; baseRepRequirement = 0;
// Description of what this Aug is and what it does // Description of what this Aug is and what it does
@ -425,9 +437,6 @@ export class Augmentation {
// Any Augmentation not immediately available in BitNode-1 is special (e.g. Bladeburner augs) // Any Augmentation not immediately available in BitNode-1 is special (e.g. Bladeburner augs)
isSpecial = false; isSpecial = false;
// Augmentation level - for repeatable Augs like NeuroFlux Governor
level = 0;
// Name of Augmentation // Name of Augmentation
name = ""; name = "";
@ -438,12 +447,6 @@ export class Augmentation {
// The Player/Person classes // The Player/Person classes
mults: IMap<number> = {}; mults: IMap<number> = {};
// Initial cost. Doesn't change when you purchase multiple Augmentation
startingCost = 0;
// Initial rep requirement. Doesn't change when you purchase multiple Augmentation
startingRepRequirement = 0;
// Factions that offer this aug. // Factions that offer this aug.
factions: string[] = []; factions: string[] = [];
@ -461,17 +464,15 @@ export class Augmentation {
this.prereqs = params.prereqs ? params.prereqs : []; this.prereqs = params.prereqs ? params.prereqs : [];
this.baseRepRequirement = params.repCost; this.baseRepRequirement = params.repCost;
Object.freeze(this.baseRepRequirement);
this.baseCost = params.moneyCost; this.baseCost = params.moneyCost;
this.startingCost = this.baseCost; Object.freeze(this.baseCost);
this.startingRepRequirement = this.baseRepRequirement;
this.factions = params.factions; this.factions = params.factions;
if (params.isSpecial) { if (params.isSpecial) {
this.isSpecial = true; this.isSpecial = true;
} }
this.level = 0;
// Set multipliers // Set multipliers
if (params.hacking_mult) { if (params.hacking_mult) {
this.mults.hacking_mult = params.hacking_mult; this.mults.hacking_mult = params.hacking_mult;
@ -600,6 +601,61 @@ export class Augmentation {
} }
} }
getCost(player: IPlayer): AugmentationCosts {
const augmentationReference = StaticAugmentations[this.name];
let moneyCost = augmentationReference.baseCost;
let repCost = augmentationReference.baseRepRequirement;
if (augmentationReference.name === AugmentationNames.NeuroFluxGovernor) {
let nextLevel = this.getLevel(player);
--nextLevel;
const multiplier = Math.pow(CONSTANTS.NeuroFluxGovernorLevelMult, nextLevel);
repCost = augmentationReference.baseRepRequirement * multiplier * BitNodeMultipliers.AugmentationRepCost;
moneyCost = augmentationReference.baseCost * multiplier * BitNodeMultipliers.AugmentationMoneyCost;
for (let i = 0; i < player.queuedAugmentations.length; ++i) {
moneyCost *= getBaseAugmentationPriceMultiplier();
}
} else if (augmentationReference.factions.includes(FactionNames.ShadowsOfAnarchy)) {
const soaAugmentationNames = initSoAAugmentations().map((augmentation) => augmentation.name);
const soaMultiplier = Math.pow(
CONSTANTS.SoACostMult,
soaAugmentationNames.filter((augmentationName) => player.hasAugmentation(augmentationName)).length,
);
moneyCost = augmentationReference.baseCost * soaMultiplier;
if (soaAugmentationNames.find((augmentationName) => augmentationName === augmentationReference.name)) {
repCost = augmentationReference.baseRepRequirement * soaMultiplier;
}
} else {
moneyCost =
augmentationReference.baseCost *
getGenericAugmentationPriceMultiplier() *
BitNodeMultipliers.AugmentationMoneyCost;
}
return { moneyCost, repCost };
}
getLevel(player: IPlayer): number {
// Get current Neuroflux level based on Player's augmentations
if (this.name === AugmentationNames.NeuroFluxGovernor) {
let currLevel = 0;
for (let i = 0; i < player.augmentations.length; ++i) {
if (player.augmentations[i].name === AugmentationNames.NeuroFluxGovernor) {
currLevel = player.augmentations[i].level;
}
}
// Account for purchased but uninstalled Augmentations
for (let i = 0; i < player.queuedAugmentations.length; ++i) {
if (player.queuedAugmentations[i].name == AugmentationNames.NeuroFluxGovernor) {
++currLevel;
}
}
return currLevel + 1;
}
return 0;
}
// Adds this Augmentation to all Factions // Adds this Augmentation to all Factions
addToAllFactions(): void { addToAllFactions(): void {
for (const fac of Object.keys(Factions)) { for (const fac of Object.keys(Factions)) {

@ -1,5 +1,5 @@
import { Augmentation } from "./Augmentation"; import { Augmentation } from "./Augmentation";
import { Augmentations } from "./Augmentations"; import { StaticAugmentations } from "./StaticAugmentations";
import { PlayerOwnedAugmentation, IPlayerOwnedAugmentation } from "./PlayerOwnedAugmentation"; import { PlayerOwnedAugmentation, IPlayerOwnedAugmentation } from "./PlayerOwnedAugmentation";
import { AugmentationNames } from "./data/AugmentationNames"; import { AugmentationNames } from "./data/AugmentationNames";
@ -20,30 +20,11 @@ import {
initNeuroFluxGovernor, initNeuroFluxGovernor,
initUnstableCircadianModulator, initUnstableCircadianModulator,
} from "./data/AugmentationCreator"; } from "./data/AugmentationCreator";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { Router } from "../ui/GameRoot"; import { Router } from "../ui/GameRoot";
export function AddToAugmentations(aug: Augmentation): void { export function AddToStaticAugmentations(aug: Augmentation): void {
const name = aug.name; const name = aug.name;
Augmentations[name] = aug; StaticAugmentations[name] = aug;
}
export function getNextNeuroFluxLevel(): number {
// Get current Neuroflux level based on Player's augmentations
let currLevel = 0;
for (let i = 0; i < Player.augmentations.length; ++i) {
if (Player.augmentations[i].name === AugmentationNames.NeuroFluxGovernor) {
currLevel = Player.augmentations[i].level;
}
}
// Account for purchased but uninstalled Augmentations
for (let i = 0; i < Player.queuedAugmentations.length; ++i) {
if (Player.queuedAugmentations[i].name == AugmentationNames.NeuroFluxGovernor) {
++currLevel;
}
}
return currLevel + 1;
} }
function createAugmentations(): void { function createAugmentations(): void {
@ -67,96 +48,37 @@ function resetFactionAugmentations(): void {
function initAugmentations(): void { function initAugmentations(): void {
resetFactionAugmentations(); resetFactionAugmentations();
clearObject(Augmentations); clearObject(StaticAugmentations);
createAugmentations(); createAugmentations();
updateAugmentationCosts();
Player.reapplyAllAugmentations(); Player.reapplyAllAugmentations();
} }
function getBaseAugmentationPriceMultiplier(): number { export function getBaseAugmentationPriceMultiplier(): number {
return CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][Player.sourceFileLvl(11)]; return CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][Player.sourceFileLvl(11)];
} }
export function getGenericAugmentationPriceMultiplier(): number { export function getGenericAugmentationPriceMultiplier(): number {
return Math.pow(getBaseAugmentationPriceMultiplier(), Player.queuedAugmentations.length); return Math.pow(getBaseAugmentationPriceMultiplier(), Player.queuedAugmentations.length);
} }
function updateNeuroFluxGovernorCosts(neuroFluxGovernorAugmentation: Augmentation): void {
let nextLevel = getNextNeuroFluxLevel();
--nextLevel;
const multiplier = Math.pow(CONSTANTS.NeuroFluxGovernorLevelMult, nextLevel);
neuroFluxGovernorAugmentation.baseRepRequirement =
neuroFluxGovernorAugmentation.startingRepRequirement * multiplier * BitNodeMultipliers.AugmentationRepCost;
neuroFluxGovernorAugmentation.baseCost =
neuroFluxGovernorAugmentation.startingCost * multiplier * BitNodeMultipliers.AugmentationMoneyCost;
for (let i = 0; i < Player.queuedAugmentations.length; ++i) {
neuroFluxGovernorAugmentation.baseCost *= getBaseAugmentationPriceMultiplier();
}
}
function updateSoACosts(soaAugmentation: Augmentation): void {
const soaAugmentationNames = initSoAAugmentations().map((augmentation) => augmentation.name);
const soaAugCount = soaAugmentationNames.filter((augmentationName) =>
Player.hasAugmentation(augmentationName),
).length;
soaAugmentation.baseCost = soaAugmentation.startingCost * Math.pow(CONSTANTS.SoACostMult, soaAugCount);
if (soaAugmentationNames.find((augmentationName) => augmentationName === soaAugmentation.name)) {
soaAugmentation.baseRepRequirement =
soaAugmentation.startingRepRequirement * Math.pow(CONSTANTS.SoARepMult, soaAugCount);
}
}
export function updateAugmentationCosts(): void {
for (const name of Object.keys(Augmentations)) {
if (Augmentations.hasOwnProperty(name)) {
const augmentationToUpdate = Augmentations[name];
if (augmentationToUpdate.name === AugmentationNames.NeuroFluxGovernor) {
updateNeuroFluxGovernorCosts(augmentationToUpdate);
} else if (augmentationToUpdate.factions.includes(FactionNames.ShadowsOfAnarchy)) {
updateSoACosts(augmentationToUpdate);
} else {
augmentationToUpdate.baseCost =
augmentationToUpdate.startingCost *
getGenericAugmentationPriceMultiplier() *
BitNodeMultipliers.AugmentationMoneyCost;
}
}
}
}
//Resets an Augmentation during (re-initizliation) //Resets an Augmentation during (re-initizliation)
function resetAugmentation(aug: Augmentation): void { function resetAugmentation(aug: Augmentation): void {
aug.addToFactions(aug.factions); aug.addToFactions(aug.factions);
const name = aug.name; const name = aug.name;
if (augmentationExists(name)) { if (augmentationExists(name)) {
delete Augmentations[name]; delete StaticAugmentations[name];
} }
AddToAugmentations(aug); AddToStaticAugmentations(aug);
} }
function applyAugmentation(aug: IPlayerOwnedAugmentation, reapply = false): void { function applyAugmentation(aug: IPlayerOwnedAugmentation, reapply = false): void {
const augObj = Augmentations[aug.name]; const staticAugmentation = StaticAugmentations[aug.name];
// Apply multipliers // Apply multipliers
for (const mult of Object.keys(augObj.mults)) { for (const mult of Object.keys(staticAugmentation.mults)) {
const v = Player.getMult(mult) * augObj.mults[mult]; const v = Player.getMult(mult) * staticAugmentation.mults[mult];
Player.setMult(mult, v); Player.setMult(mult, v);
} }
// Special logic for NeuroFlux Governor
if (aug.name === AugmentationNames.NeuroFluxGovernor) {
if (!reapply) {
Augmentations[aug.name].level = aug.level;
for (let i = 0; i < Player.augmentations.length; ++i) {
if (Player.augmentations[i].name == AugmentationNames.NeuroFluxGovernor) {
Player.augmentations[i].level = aug.level;
return;
// break;
}
}
}
}
// Special logic for Congruity Implant // Special logic for Congruity Implant
if (aug.name === AugmentationNames.CongruityImplant && !reapply) { if (aug.name === AugmentationNames.CongruityImplant && !reapply) {
Player.entropy = 0; Player.entropy = 0;
@ -185,7 +107,7 @@ function installAugmentations(force?: boolean): boolean {
} }
for (let i = 0; i < Player.queuedAugmentations.length; ++i) { for (let i = 0; i < Player.queuedAugmentations.length; ++i) {
const ownedAug = Player.queuedAugmentations[i]; const ownedAug = Player.queuedAugmentations[i];
const aug = Augmentations[ownedAug.name]; const aug = StaticAugmentations[ownedAug.name];
if (aug == null) { if (aug == null) {
console.error(`Invalid augmentation: ${ownedAug.name}`); console.error(`Invalid augmentation: ${ownedAug.name}`);
continue; continue;
@ -215,7 +137,7 @@ function installAugmentations(force?: boolean): boolean {
} }
function augmentationExists(name: string): boolean { function augmentationExists(name: string): boolean {
return Augmentations.hasOwnProperty(name); return StaticAugmentations.hasOwnProperty(name);
} }
export function isRepeatableAug(aug: Augmentation): boolean { export function isRepeatableAug(aug: Augmentation): boolean {

@ -1,4 +1,4 @@
import { Augmentation } from "./Augmentation"; import { Augmentation } from "./Augmentation";
import { IMap } from "../types"; import { IMap } from "../types";
export const Augmentations: IMap<Augmentation> = {}; export const StaticAugmentations: IMap<Augmentation> = {};

@ -114,17 +114,6 @@ export enum AugmentationNames {
StaneksGift2 = "Stanek's Gift - Awakening", StaneksGift2 = "Stanek's Gift - Awakening",
StaneksGift3 = "Stanek's Gift - Serenity", StaneksGift3 = "Stanek's Gift - Serenity",
/*
MightOfAres = "Might of Ares", // slash
WisdomOfAthena = "Wisdom of Athena", // bracket
TrickeryOfHermes = "Trickery of Hermes", // cheatcode
BeautyOfAphrodite = "Beauty of Aphrodite", // bribe
ChaosOfDionysus = "Chaos of Dionysus", // reverse
FloodOfPoseidon = "Flood of Poseidon", // hex
HuntOfArtemis = "Hunt of Artemis", // mine
KnowledgeOfApollo = "Knowledge of Apollo", // wire
*/
// Infiltrators MiniGames // Infiltrators MiniGames
MightOfAres = "SoA - Might of Ares", // slash MightOfAres = "SoA - Might of Ares", // slash
WisdomOfAthena = "SoA - Wisdom of Athena", // bracket WisdomOfAthena = "SoA - Wisdom of Athena", // bracket
@ -135,10 +124,4 @@ export enum AugmentationNames {
HuntOfArtemis = "SoA - Hunt of Artemis", // mine HuntOfArtemis = "SoA - Hunt of Artemis", // mine
KnowledgeOfApollo = "SoA - Knowledge of Apollo", // wire KnowledgeOfApollo = "SoA - Knowledge of Apollo", // wire
WKSharmonizer = "SoA - phyzical WKS harmonizer", WKSharmonizer = "SoA - phyzical WKS harmonizer",
//Wasteland Augs
//PepBoy: "P.E.P-Boy", Plasma Energy Projection System
//PepBoyForceField Generates plasma force fields
//PepBoyBlasts Generate high density plasma concussive blasts
//PepBoyDataStorage STore more data on pep boy,
} }

@ -22,7 +22,7 @@ import { Settings } from "../../Settings/Settings";
import { ConfirmationModal } from "../../ui/React/ConfirmationModal"; import { ConfirmationModal } from "../../ui/React/ConfirmationModal";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { AugmentationNames } from "../data/AugmentationNames"; import { AugmentationNames } from "../data/AugmentationNames";
import { Augmentations } from "../Augmentations"; import { StaticAugmentations } from "../StaticAugmentations";
import { CONSTANTS } from "../../Constants"; import { CONSTANTS } from "../../Constants";
import { formatNumber } from "../../utils/StringHelperFunctions"; import { formatNumber } from "../../utils/StringHelperFunctions";
import { Info } from "@mui/icons-material"; import { Info } from "@mui/icons-material";
@ -39,7 +39,9 @@ const NeuroFluxDisplay = ({ player }: NFGDisplayProps): React.ReactElement => {
<Typography variant="h5" color={Settings.theme.info}> <Typography variant="h5" color={Settings.theme.info}>
NeuroFlux Governor - Level {level} NeuroFlux Governor - Level {level}
</Typography> </Typography>
<Typography color={Settings.theme.info}>{Augmentations[AugmentationNames.NeuroFluxGovernor].stats}</Typography> <Typography color={Settings.theme.info}>
{StaticAugmentations[AugmentationNames.NeuroFluxGovernor].stats}
</Typography>
</Paper> </Paper>
) : ( ) : (
<></> <></>

@ -13,7 +13,7 @@ import React, { useState } from "react";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums"; import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { Augmentations } from "../Augmentations"; import { StaticAugmentations } from "../StaticAugmentations";
import { AugmentationNames } from "../data/AugmentationNames"; import { AugmentationNames } from "../data/AugmentationNames";
export function InstalledAugmentations(): React.ReactElement { export function InstalledAugmentations(): React.ReactElement {
@ -77,7 +77,7 @@ export function InstalledAugmentations(): React.ReactElement {
</Typography> </Typography>
<Typography sx={{ maxHeight: 350, overflowY: "scroll" }}> <Typography sx={{ maxHeight: 350, overflowY: "scroll" }}>
{(() => { {(() => {
const aug = Augmentations[selectedAug.name]; const aug = StaticAugmentations[selectedAug.name];
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info; const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const tooltip = ( const tooltip = (

@ -8,7 +8,7 @@ import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
import { Player } from "../../Player"; import { Player } from "../../Player";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { Augmentations } from "../Augmentations"; import { StaticAugmentations } from "../StaticAugmentations";
interface IAugmentedStats { interface IAugmentedStats {
[index: string]: number; [index: string]: number;
@ -17,7 +17,7 @@ interface IAugmentedStats {
function calculateAugmentedStats(): IAugmentedStats { function calculateAugmentedStats(): IAugmentedStats {
const augP: IAugmentedStats = {}; const augP: IAugmentedStats = {};
for (const aug of Player.queuedAugmentations) { for (const aug of Player.queuedAugmentations) {
const augObj = Augmentations[aug.name]; const augObj = StaticAugmentations[aug.name];
for (const mult of Object.keys(augObj.mults)) { for (const mult of Object.keys(augObj.mults)) {
const v = augP[mult] ? augP[mult] : 1; const v = augP[mult] ? augP[mult] : 1;
augP[mult] = v * augObj.mults[mult]; augP[mult] = v * augObj.mults[mult];

@ -5,15 +5,14 @@
import { CheckBox, CheckBoxOutlineBlank, CheckCircle, Info, NewReleases, Report } from "@mui/icons-material"; import { CheckBox, CheckBoxOutlineBlank, CheckCircle, Info, NewReleases, Report } from "@mui/icons-material";
import { Box, Button, Container, Paper, Tooltip, Typography } from "@mui/material"; import { Box, Button, Container, Paper, Tooltip, Typography } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import { getNextNeuroFluxLevel } from "../AugmentationHelpers";
import { Faction } from "../../Faction/Faction"; import { Faction } from "../../Faction/Faction";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
import { Augmentation } from "../Augmentation"; import { Augmentation } from "../Augmentation";
import { Augmentations } from "../Augmentations";
import { AugmentationNames } from "../data/AugmentationNames"; import { AugmentationNames } from "../data/AugmentationNames";
import { PurchaseAugmentationModal } from "./PurchaseAugmentationModal"; import { PurchaseAugmentationModal } from "./PurchaseAugmentationModal";
import { StaticAugmentations } from "../StaticAugmentations";
interface IPreReqsProps { interface IPreReqsProps {
player: IPlayer; player: IPlayer;
@ -160,10 +159,10 @@ interface IPurchasableAugProps {
export function PurchasableAugmentation(props: IPurchasableAugProps): React.ReactElement { export function PurchasableAugmentation(props: IPurchasableAugProps): React.ReactElement {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const aug = Augmentations[props.augName]; const aug = StaticAugmentations[props.augName];
const augCosts = aug.getCost(props.parent.player);
const cost = props.parent.sleeveAugs ? aug.startingCost : aug.baseCost; const cost = props.parent.sleeveAugs ? aug.baseCost : augCosts.moneyCost;
const repCost = augCosts.repCost;
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info; const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const description = ( const description = (
<> <>
@ -205,7 +204,8 @@ export function PurchasableAugmentation(props: IPurchasableAugProps): React.Reac
<> <>
<Typography variant="h5"> <Typography variant="h5">
{props.augName} {props.augName}
{props.augName === AugmentationNames.NeuroFluxGovernor && ` - Level ${getNextNeuroFluxLevel()}`} {props.augName === AugmentationNames.NeuroFluxGovernor &&
` - Level ${aug.getLevel(props.parent.player)}`}
</Typography> </Typography>
<Typography>{description}</Typography> <Typography>{description}</Typography>
</> </>
@ -222,7 +222,7 @@ export function PurchasableAugmentation(props: IPurchasableAugProps): React.Reac
}} }}
> >
{aug.name} {aug.name}
{aug.name === AugmentationNames.NeuroFluxGovernor && ` - Level ${getNextNeuroFluxLevel()}`} {aug.name === AugmentationNames.NeuroFluxGovernor && ` - Level ${aug.getLevel(props.parent.player)}`}
</Typography> </Typography>
{aug.factions.length === 1 && !props.parent.sleeveAugs && ( {aug.factions.length === 1 && !props.parent.sleeveAugs && (
<Exclusive player={props.parent.player} aug={aug} /> <Exclusive player={props.parent.player} aug={aug} />
@ -236,14 +236,14 @@ export function PurchasableAugmentation(props: IPurchasableAugProps): React.Reac
{props.owned || ( {props.owned || (
<Box sx={{ display: "grid", alignItems: "center", justifyItems: "left" }}> <Box sx={{ display: "grid", alignItems: "center", justifyItems: "left" }}>
<Requirement <Requirement
fulfilled={aug.baseCost === 0 || props.parent.player.money > cost} fulfilled={cost === 0 || props.parent.player.money > cost}
value={numeralWrapper.formatMoney(cost)} value={numeralWrapper.formatMoney(cost)}
color={Settings.theme.money} color={Settings.theme.money}
/> />
{props.parent.rep !== undefined && ( {props.parent.rep !== undefined && (
<Requirement <Requirement
fulfilled={props.parent.rep >= aug.baseRepRequirement} fulfilled={props.parent.rep >= repCost}
value={`${numeralWrapper.formatReputation(aug.baseRepRequirement)} rep`} value={`${numeralWrapper.formatReputation(repCost)} rep`}
color={Settings.theme.rep} color={Settings.theme.rep}
/> />
)} )}

@ -44,7 +44,7 @@ export function PurchaseAugmentationModal(props: IProps): React.ReactElement {
<br /> <br />
<br /> <br />
Would you like to purchase the {props.aug.name} Augmentation for&nbsp; Would you like to purchase the {props.aug.name} Augmentation for&nbsp;
<Money money={props.aug.baseCost} />? <Money money={props.aug.getCost(player).moneyCost} />?
<br /> <br />
<br /> <br />
</Typography> </Typography>

@ -5,7 +5,7 @@
import { List, ListItemText, Paper, Tooltip, Typography } from "@mui/material"; import { List, ListItemText, Paper, Tooltip, Typography } from "@mui/material";
import * as React from "react"; import * as React from "react";
import { Player } from "../../Player"; import { Player } from "../../Player";
import { Augmentations } from "../Augmentations"; import { StaticAugmentations } from "../StaticAugmentations";
import { AugmentationNames } from "../data/AugmentationNames"; import { AugmentationNames } from "../data/AugmentationNames";
export function PurchasedAugmentations(): React.ReactElement { export function PurchasedAugmentations(): React.ReactElement {
@ -23,7 +23,7 @@ export function PurchasedAugmentations(): React.ReactElement {
let displayName = ownedAug.name; let displayName = ownedAug.name;
if (ownedAug.name === AugmentationNames.NeuroFluxGovernor && i !== nfgIndex) continue; if (ownedAug.name === AugmentationNames.NeuroFluxGovernor && i !== nfgIndex) continue;
const aug = Augmentations[ownedAug.name]; const aug = StaticAugmentations[ownedAug.name];
let level = null; let level = null;
if (ownedAug.name === AugmentationNames.NeuroFluxGovernor) { if (ownedAug.name === AugmentationNames.NeuroFluxGovernor) {

@ -293,7 +293,7 @@ export const CONSTANTS: {
// BitNode/Source-File related stuff // BitNode/Source-File related stuff
TotalNumBitNodes: 24, TotalNumBitNodes: 24,
Donations: 6, Donations: 7,
LatestUpdate: ` LatestUpdate: `
v1.6.3 - 2022-04-01 Few stanek fixes v1.6.3 - 2022-04-01 Few stanek fixes

@ -162,11 +162,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
<Tooltip <Tooltip
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""} title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
> >
<Button <Button color={tutorial ? "error" : "primary"} onClick={() => setPurchaseMaterialOpen(true)}>
color={tutorial ? "error" : "primary"}
onClick={() => setPurchaseMaterialOpen(true)}
disabled={props.warehouse.smartSupplyEnabled && Object.keys(division.reqMats).includes(props.mat.name)}
>
{purchaseButtonText} {purchaseButtonText}
</Button> </Button>
</Tooltip> </Tooltip>
@ -174,6 +170,9 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
mat={mat} mat={mat}
warehouse={warehouse} warehouse={warehouse}
open={purchaseMaterialOpen} open={purchaseMaterialOpen}
disablePurchaseLimit={
props.warehouse.smartSupplyEnabled && Object.keys(division.reqMats).includes(props.mat.name)
}
onClose={() => setPurchaseMaterialOpen(false)} onClose={() => setPurchaseMaterialOpen(false)}
/> />

@ -106,6 +106,7 @@ interface IProps {
onClose: () => void; onClose: () => void;
mat: Material; mat: Material;
warehouse: Warehouse; warehouse: Warehouse;
disablePurchaseLimit: boolean;
} }
// Create a popup that lets the player purchase a Material // Create a popup that lets the player purchase a Material
@ -143,6 +144,7 @@ export function PurchaseMaterialModal(props: IProps): React.ReactElement {
<Typography> <Typography>
Enter the amount of {props.mat.name} you would like to purchase per second. This material's cost changes Enter the amount of {props.mat.name} you would like to purchase per second. This material's cost changes
constantly. constantly.
{props.disablePurchaseLimit ? "Note: Purchase amount is disabled as smart supply is enabled" : ""}
</Typography> </Typography>
<TextField <TextField
value={buyAmt} value={buyAmt}
@ -150,10 +152,15 @@ export function PurchaseMaterialModal(props: IProps): React.ReactElement {
autoFocus={true} autoFocus={true}
placeholder="Purchase amount" placeholder="Purchase amount"
type="number" type="number"
disabled={props.disablePurchaseLimit}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
/> />
<Button onClick={purchaseMaterial}>Confirm</Button> <Button disabled={props.disablePurchaseLimit} onClick={purchaseMaterial}>
<Button onClick={clearPurchase}>Clear Purchase</Button> Confirm
</Button>
<Button disabled={props.disablePurchaseLimit} onClick={clearPurchase}>
Clear Purchase
</Button>
{division.hasResearch("Bulk Purchasing") && ( {division.hasResearch("Bulk Purchasing") && (
<BulkPurchaseSection onClose={props.onClose} mat={props.mat} warehouse={props.warehouse} /> <BulkPurchaseSection onClose={props.onClose} mat={props.mat} warehouse={props.warehouse} />
)} )}

@ -1,4 +1,4 @@
import { Augmentations } from "../Augmentation/Augmentations"; import { StaticAugmentations } from "../Augmentation/StaticAugmentations";
import { Augmentation } from "../Augmentation/Augmentation"; import { Augmentation } from "../Augmentation/Augmentation";
import { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; import { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
@ -18,7 +18,6 @@ import {
import { dialogBoxCreate } from "../ui/React/DialogBox"; import { dialogBoxCreate } from "../ui/React/DialogBox";
import { InvitationEvent } from "./ui/InvitationModal"; import { InvitationEvent } from "./ui/InvitationModal";
import { FactionNames } from "./data/FactionNames"; import { FactionNames } from "./data/FactionNames";
import { updateAugmentationCosts, getNextNeuroFluxLevel } from "../Augmentation/AugmentationHelpers";
import { SFC32RNG } from "../Casino/RNG"; import { SFC32RNG } from "../Casino/RNG";
export function inviteToFaction(faction: Faction): void { export function inviteToFaction(faction: Faction): void {
@ -59,6 +58,7 @@ export function hasAugmentationPrereqs(aug: Augmentation): boolean {
export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = false): string { export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = false): string {
const hasPrereqs = hasAugmentationPrereqs(aug); const hasPrereqs = hasAugmentationPrereqs(aug);
const augCosts = aug.getCost(Player);
if (!hasPrereqs) { if (!hasPrereqs) {
const txt = `You must first purchase or install ${aug.prereqs const txt = `You must first purchase or install ${aug.prereqs
.filter((req) => !Player.hasAugmentation(req)) .filter((req) => !Player.hasAugmentation(req))
@ -68,28 +68,26 @@ export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = fal
} else { } else {
dialogBoxCreate(txt); dialogBoxCreate(txt);
} }
} else if (aug.baseCost !== 0 && Player.money < aug.baseCost) { } else if (augCosts.moneyCost !== 0 && Player.money < augCosts.moneyCost) {
const txt = "You don't have enough money to purchase " + aug.name; const txt = "You don't have enough money to purchase " + aug.name;
if (sing) { if (sing) {
return txt; return txt;
} }
dialogBoxCreate(txt); dialogBoxCreate(txt);
} else if (fac.playerReputation < aug.baseRepRequirement) { } else if (fac.playerReputation < augCosts.repCost) {
const txt = "You don't have enough faction reputation to purchase " + aug.name; const txt = "You don't have enough faction reputation to purchase " + aug.name;
if (sing) { if (sing) {
return txt; return txt;
} }
dialogBoxCreate(txt); dialogBoxCreate(txt);
} else if (aug.baseCost === 0 || Player.money >= aug.baseCost) { } else if (augCosts.moneyCost === 0 || Player.money >= augCosts.moneyCost) {
const queuedAugmentation = new PlayerOwnedAugmentation(aug.name); const queuedAugmentation = new PlayerOwnedAugmentation(aug.name);
if (aug.name == AugmentationNames.NeuroFluxGovernor) { if (aug.name == AugmentationNames.NeuroFluxGovernor) {
queuedAugmentation.level = getNextNeuroFluxLevel(); queuedAugmentation.level = aug.getLevel(Player);
} }
Player.queuedAugmentations.push(queuedAugmentation); Player.queuedAugmentations.push(queuedAugmentation);
Player.loseMoney(aug.baseCost, "augmentations"); Player.loseMoney(augCosts.moneyCost, "augmentations");
updateAugmentationCosts();
if (sing) { if (sing) {
return "You purchased " + aug.name; return "You purchased " + aug.name;
@ -141,14 +139,14 @@ export function processPassiveFactionRepGain(numCycles: number): void {
export const getFactionAugmentationsFiltered = (player: IPlayer, faction: Faction): string[] => { export const getFactionAugmentationsFiltered = (player: IPlayer, faction: Faction): string[] => {
// If player has a gang with this faction, return (almost) all augmentations // If player has a gang with this faction, return (almost) all augmentations
if (player.hasGangWith(faction.name)) { if (player.hasGangWith(faction.name)) {
let augs = Object.values(Augmentations); let augs = Object.values(StaticAugmentations);
// Remove special augs // Remove special augs
augs = augs.filter((a) => !a.isSpecial || a.name != AugmentationNames.CongruityImplant); augs = augs.filter((a) => !a.isSpecial && a.name !== AugmentationNames.CongruityImplant);
if (player.bitNodeN === 2) { if (player.bitNodeN === 2) {
// TRP is not available outside of BN2 for Gangs // TRP is not available outside of BN2 for Gangs
augs.push(Augmentations[AugmentationNames.TheRedPill]); augs.push(StaticAugmentations[AugmentationNames.TheRedPill]);
} }
const rng = SFC32RNG(`BN${player.bitNodeN}.${player.sourceFileLvl(player.bitNodeN)}`); const rng = SFC32RNG(`BN${player.bitNodeN}.${player.sourceFileLvl(player.bitNodeN)}`);

@ -3,8 +3,9 @@
*/ */
import { Box, Button, Tooltip, Typography, Paper, Container } from "@mui/material"; import { Box, Button, Tooltip, Typography, Paper, Container } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import { StaticAugmentations } from "../../Augmentation/StaticAugmentations";
import { getGenericAugmentationPriceMultiplier } from "../../Augmentation/AugmentationHelpers"; import { getGenericAugmentationPriceMultiplier } from "../../Augmentation/AugmentationHelpers";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { PurchasableAugmentations } from "../../Augmentation/ui/PurchasableAugmentations"; import { PurchasableAugmentations } from "../../Augmentation/ui/PurchasableAugmentations";
import { PurchaseAugmentationsOrderSetting } from "../../Settings/SettingEnums"; import { PurchaseAugmentationsOrderSetting } from "../../Settings/SettingEnums";
@ -54,13 +55,13 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
function getAugsSortedByCost(): string[] { function getAugsSortedByCost(): string[] {
const augs = getAugs(); const augs = getAugs();
augs.sort((augName1, augName2) => { augs.sort((augName1, augName2) => {
const aug1 = Augmentations[augName1], const aug1 = StaticAugmentations[augName1],
aug2 = Augmentations[augName2]; aug2 = StaticAugmentations[augName2];
if (aug1 == null || aug2 == null) { if (aug1 == null || aug2 == null) {
throw new Error("Invalid Augmentation Names"); throw new Error("Invalid Augmentation Names");
} }
return aug1.baseCost - aug2.baseCost; return aug1.getCost(player).moneyCost - aug2.getCost(player).moneyCost;
}); });
return augs; return augs;
@ -69,31 +70,32 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
function getAugsSortedByPurchasable(): string[] { function getAugsSortedByPurchasable(): string[] {
const augs = getAugs(); const augs = getAugs();
function canBuy(augName: string): boolean { function canBuy(augName: string): boolean {
const aug = Augmentations[augName]; const aug = StaticAugmentations[augName];
const repCost = aug.baseRepRequirement; const augCosts = aug.getCost(player);
const repCost = augCosts.repCost;
const hasReq = props.faction.playerReputation >= repCost; const hasReq = props.faction.playerReputation >= repCost;
const hasRep = hasAugmentationPrereqs(aug); const hasRep = hasAugmentationPrereqs(aug);
const hasCost = aug.baseCost !== 0 && player.money > aug.baseCost; const hasCost = augCosts.moneyCost !== 0 && player.money > augCosts.moneyCost;
return hasCost && hasReq && hasRep; return hasCost && hasReq && hasRep;
} }
const buy = augs.filter(canBuy).sort((augName1, augName2) => { const buy = augs.filter(canBuy).sort((augName1, augName2) => {
const aug1 = Augmentations[augName1], const aug1 = StaticAugmentations[augName1],
aug2 = Augmentations[augName2]; aug2 = StaticAugmentations[augName2];
if (aug1 == null || aug2 == null) { if (aug1 == null || aug2 == null) {
throw new Error("Invalid Augmentation Names"); throw new Error("Invalid Augmentation Names");
} }
return aug1.baseCost - aug2.baseCost; return aug1.getCost(player).moneyCost - aug2.getCost(player).moneyCost;
}); });
const cantBuy = augs const cantBuy = augs
.filter((aug) => !canBuy(aug)) .filter((aug) => !canBuy(aug))
.sort((augName1, augName2) => { .sort((augName1, augName2) => {
const aug1 = Augmentations[augName1], const aug1 = StaticAugmentations[augName1],
aug2 = Augmentations[augName2]; aug2 = StaticAugmentations[augName2];
if (aug1 == null || aug2 == null) { if (aug1 == null || aug2 == null) {
throw new Error("Invalid Augmentation Names"); throw new Error("Invalid Augmentation Names");
} }
return aug1.baseRepRequirement - aug2.baseRepRequirement; return aug1.getCost(player).repCost - aug2.getCost(player).repCost;
}); });
return buy.concat(cantBuy); return buy.concat(cantBuy);
@ -102,12 +104,12 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
function getAugsSortedByReputation(): string[] { function getAugsSortedByReputation(): string[] {
const augs = getAugs(); const augs = getAugs();
augs.sort((augName1, augName2) => { augs.sort((augName1, augName2) => {
const aug1 = Augmentations[augName1], const aug1 = StaticAugmentations[augName1],
aug2 = Augmentations[augName2]; aug2 = StaticAugmentations[augName2];
if (aug1 == null || aug2 == null) { if (aug1 == null || aug2 == null) {
throw new Error("Invalid Augmentation Names"); throw new Error("Invalid Augmentation Names");
} }
return aug1.baseRepRequirement - aug2.baseRepRequirement; return aug1.getCost(player).repCost - aug2.getCost(player).repCost;
}); });
return augs; return augs;
@ -195,10 +197,11 @@ export function AugmentationsPage(props: IProps): React.ReactElement {
ownedAugNames={owned} ownedAugNames={owned}
player={player} player={player}
canPurchase={(player, aug) => { canPurchase={(player, aug) => {
const costs = aug.getCost(player);
return ( return (
hasAugmentationPrereqs(aug) && hasAugmentationPrereqs(aug) &&
props.faction.playerReputation >= aug.baseRepRequirement && props.faction.playerReputation >= costs.repCost &&
(aug.baseCost === 0 || player.money > aug.baseCost) (costs.moneyCost === 0 || player.money > costs.moneyCost)
); );
}} }}
purchaseAugmentation={(player, aug, showModal) => { purchaseAugmentation={(player, aug, showModal) => {

@ -16,12 +16,10 @@ import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
import { Faction } from "../Faction"; import { Faction } from "../Faction";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { CreateGangModal } from "./CreateGangModal";
import { Box, Paper, Typography, Button, Tooltip } from "@mui/material"; import { Typography, Button } from "@mui/material";
import { CovenantPurchasesRoot } from "../../PersonObjects/Sleeve/ui/CovenantPurchasesRoot"; import { CovenantPurchasesRoot } from "../../PersonObjects/Sleeve/ui/CovenantPurchasesRoot";
import { FactionNames } from "../data/FactionNames"; import { FactionNames } from "../data/FactionNames";
import { GangConstants } from "../../Gang/data/Constants";
import { GangButton } from "./GangButton"; import { GangButton } from "./GangButton";
type IProps = { type IProps = {
@ -30,7 +28,6 @@ type IProps = {
}; };
// Info text for all options on the UI // Info text for all options on the UI
const gangInfo = "Create and manage a gang for this Faction. Gangs will earn you money and faction reputation";
const hackingContractsInfo = const hackingContractsInfo =
"Complete hacking contracts for your faction. " + "Complete hacking contracts for your faction. " +
"Your effectiveness, which determines how much " + "Your effectiveness, which determines how much " +

@ -189,9 +189,9 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
title={ title={
<Typography> <Typography>
Forcefully kill all active running scripts, in case there is a bug or some unexpected issue with the game. Forcefully kill all active running scripts, in case there is a bug or some unexpected issue with the game.
After using this, save the game and then reload the page. This is different then normal kill in that After using this, save the game and then reload the page. This is different than normal kill in that
normal kill will tell the script to shut down while force kill just removes the references to it (and it normal kill will tell the script to shut down while force kill just removes the references to it (and it
should crash on it's own). This will not remove the files on your computer. Just forcefully kill all should crash on its own). This will not remove the files on your computer, just forcefully kill all
running instances of all scripts. running instances of all scripts.
</Typography> </Typography>
} }
@ -210,7 +210,7 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
title={ title={
<Typography> <Typography>
If your save file is extremely big you can use this button to view a map of all the files on every server. If your save file is extremely big you can use this button to view a map of all the files on every server.
Be careful there might be spoilers. Be careful: there might be spoilers.
</Typography> </Typography>
} }
> >

@ -60,15 +60,22 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
multiplier = Math.min(levelsToMax, purchaseMult as number); multiplier = Math.min(levelsToMax, purchaseMult as number);
} }
const increase = const base_increase =
calculateHashGainRate(node.level + multiplier, 0, node.maxRam, node.cores, props.player.hacknet_node_money_mult) - calculateHashGainRate(node.level + multiplier, 0, node.maxRam, node.cores, props.player.hacknet_node_money_mult) -
node.hashRate; calculateHashGainRate(node.level, 0, node.maxRam, node.cores, props.player.hacknet_node_money_mult);
const modded_increase = (base_increase * (node.maxRam - node.ramUsed)) / node.maxRam;
const upgradeLevelCost = node.calculateLevelUpgradeCost(multiplier, props.player.hacknet_node_level_cost_mult); const upgradeLevelCost = node.calculateLevelUpgradeCost(multiplier, props.player.hacknet_node_level_cost_mult);
upgradeLevelButton = ( upgradeLevelButton = (
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
+<HashRate hashes={increase} /> +<HashRate hashes={modded_increase} /> (effective increase, taking current RAM usage into account)
<br />
<span style={{ opacity: 0.5 }}>
+<HashRate hashes={base_increase} />
</span>{" "}
(base increase, attained when no script is running)
</Typography> </Typography>
} }
> >
@ -109,20 +116,36 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
multiplier = Math.min(levelsToMax, purchaseMult as number); multiplier = Math.min(levelsToMax, purchaseMult as number);
} }
const increase = const base_increase =
calculateHashGainRate( calculateHashGainRate(
node.level, node.level,
0, 0,
node.maxRam * Math.pow(2, multiplier), node.maxRam * Math.pow(2, multiplier),
node.cores, node.cores,
props.player.hacknet_node_money_mult, props.player.hacknet_node_money_mult,
) - node.hashRate; ) - calculateHashGainRate(node.level, 0, node.maxRam, node.cores, props.player.hacknet_node_money_mult);
const modded_increase =
calculateHashGainRate(
node.level,
node.ramUsed,
node.maxRam * Math.pow(2, multiplier),
node.cores,
props.player.hacknet_node_money_mult,
) -
calculateHashGainRate(node.level, node.ramUsed, node.maxRam, node.cores, props.player.hacknet_node_money_mult);
const upgradeRamCost = node.calculateRamUpgradeCost(multiplier, props.player.hacknet_node_ram_cost_mult); const upgradeRamCost = node.calculateRamUpgradeCost(multiplier, props.player.hacknet_node_ram_cost_mult);
upgradeRamButton = ( upgradeRamButton = (
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
+<HashRate hashes={increase} /> +<HashRate hashes={modded_increase} /> (effective increase, taking current RAM usage into account)
<br />
<span style={{ opacity: 0.5 }}>
+<HashRate hashes={base_increase} />
</span>{" "}
(base increase, attained when no script is running)
</Typography> </Typography>
} }
> >
@ -155,15 +178,22 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
multiplier = Math.min(levelsToMax, purchaseMult as number); multiplier = Math.min(levelsToMax, purchaseMult as number);
} }
const increase = const base_increase =
calculateHashGainRate(node.level, 0, node.maxRam, node.cores + multiplier, props.player.hacknet_node_money_mult) - calculateHashGainRate(node.level, 0, node.maxRam, node.cores + multiplier, props.player.hacknet_node_money_mult) -
node.hashRate; calculateHashGainRate(node.level, 0, node.maxRam, node.cores, props.player.hacknet_node_money_mult);
const modded_increase = (base_increase * (node.maxRam - node.ramUsed)) / node.maxRam;
const upgradeCoreCost = node.calculateCoreUpgradeCost(multiplier, props.player.hacknet_node_core_cost_mult); const upgradeCoreCost = node.calculateCoreUpgradeCost(multiplier, props.player.hacknet_node_core_cost_mult);
upgradeCoresButton = ( upgradeCoresButton = (
<Tooltip <Tooltip
title={ title={
<Typography> <Typography>
+<HashRate hashes={increase} /> +<HashRate hashes={modded_increase} /> (effective increase, taking current RAM usage into account)
<br />
<span style={{ opacity: 0.5 }}>
+<HashRate hashes={base_increase} />
</span>{" "}
(base increase, attained when no script is running)
</Typography> </Typography>
} }
> >
@ -232,9 +262,31 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
<Typography>Production:</Typography> <Typography>Production:</Typography>
</TableCell> </TableCell>
<TableCell colSpan={2}> <TableCell colSpan={2}>
<Typography> <Tooltip
<Hashes hashes={node.totalHashesGenerated} /> (<HashRate hashes={node.hashRate} />) title={
</Typography> <Typography>
<Hashes hashes={node.totalHashesGenerated} /> hashes produced by this server since last augment
installation.
<br />
<HashRate hashes={node.hashRate} /> current production rate.
<br />
<span style={{ opacity: 0.5 }}>
<HashRate hashes={(node.hashRate * node.maxRam) / (node.maxRam - node.ramUsed)} />
</span>{" "}
max production rate. (achieved when 100% RAM is allocated to it)
<br />
{numeralWrapper.formatRAM(node.ramUsed)} / {numeralWrapper.formatRAM(node.maxRam)} (
{Math.round((100 * node.ramUsed) / node.maxRam)}%) RAM allocated to script.
<br />
{numeralWrapper.formatRAM(node.maxRam - node.ramUsed)} / {numeralWrapper.formatRAM(node.maxRam)} (
{Math.round((100 * (node.maxRam - node.ramUsed)) / node.maxRam)}%) RAM allocated to hash production.
</Typography>
}
>
<Typography>
<Hashes hashes={node.totalHashesGenerated} /> (<HashRate hashes={node.hashRate} />)
</Typography>
</Tooltip>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>

@ -1,15 +1,14 @@
import { Paper, Typography } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import Grid from "@mui/material/Grid"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player";
import { KEY } from "../../utils/helpers/keyCodes";
import { random } from "../utils";
import { BlinkingCursor } from "./BlinkingCursor";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps"; import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler"; import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { random } from "../utils";
import { interpolate } from "./Difficulty";
import { BlinkingCursor } from "./BlinkingCursor";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
import { Player } from "../../Player";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -48,24 +47,18 @@ export function BackwardGame(props: IMinigameProps): React.ReactElement {
} }
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={timer} onExpire={props.onFailure} /> <GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center", pb: 1 }}>
<Typography variant="h4">Type it{!hasAugment ? " backward" : ""}</Typography> <Typography variant="h4">Type it{!hasAugment ? " backward" : ""}</Typography>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid> <Typography style={{ transform: hasAugment ? "none" : "scaleX(-1)" }}>{answer}</Typography>
<Grid item xs={6}>
<Typography style={{ transform: hasAugment ? "none" : "scaleX(-1)", marginLeft: hasAugment ? "50%" : "none" }}>
{answer}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography> <Typography>
{guess} {guess}
<BlinkingCursor /> <BlinkingCursor />
</Typography> </Typography>
</Grid> </Paper>
</Grid> </>
); );
} }

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useEffect, useState } from "react";
export function BlinkingCursor(): React.ReactElement { export function BlinkingCursor(): React.ReactElement {
const [on, setOn] = useState(true); const [on, setOn] = useState(true);
@ -6,5 +6,5 @@ export function BlinkingCursor(): React.ReactElement {
const i = setInterval(() => setOn((old) => !old), 1000); const i = setInterval(() => setOn((old) => !old), 1000);
return () => clearInterval(i); return () => clearInterval(i);
}); });
return <>{on ? "|" : ""}</>; return <>{on ? "|" : <>&nbsp;</>}</>;
} }

@ -1,15 +1,14 @@
import { Paper, Typography } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import Grid from "@mui/material/Grid"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player";
import { KEY } from "../../utils/helpers/keyCodes";
import { random } from "../utils";
import { BlinkingCursor } from "./BlinkingCursor";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps"; import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler"; import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { random } from "../utils";
import { interpolate } from "./Difficulty";
import { BlinkingCursor } from "./BlinkingCursor";
import Typography from "@mui/material/Typography";
import { Player } from "../../Player";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { KEY } from "../../utils/helpers/keyCodes";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -84,16 +83,16 @@ export function BracketGame(props: IMinigameProps): React.ReactElement {
} }
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={timer} onExpire={props.onFailure} /> <GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center" }}>
<Typography variant="h4">Close the brackets</Typography> <Typography variant="h4">Close the brackets</Typography>
<Typography style={{ fontSize: "5em" }}> <Typography style={{ fontSize: "5em" }}>
{`${left}${right}`} {`${left}${right}`}
<BlinkingCursor /> <BlinkingCursor />
</Typography> </Typography>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid> </Paper>
</Grid> </>
); );
} }

@ -1,15 +1,14 @@
import { Paper, Typography } from "@mui/material";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Grid from "@mui/material/Grid";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player"; import { Player } from "../../Player";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { KEY } from "../../utils/helpers/keyCodes";
import { downArrowSymbol, upArrowSymbol } from "../utils"; import { downArrowSymbol, upArrowSymbol } from "../utils";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -88,13 +87,11 @@ export function BribeGame(props: IMinigameProps): React.ReactElement {
} }
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={timer} onExpire={props.onFailure} /> <GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center" }}>
<Typography variant="h4">Say something nice about the guard.</Typography> <Typography variant="h4">Say something nice about the guard</Typography>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid>
<Grid item xs={6}>
<Typography variant="h5" color={upColor}> <Typography variant="h5" color={upColor}>
{upArrowSymbol} {upArrowSymbol}
</Typography> </Typography>
@ -104,8 +101,8 @@ export function BribeGame(props: IMinigameProps): React.ReactElement {
<Typography variant="h5" color={downColor}> <Typography variant="h5" color={downColor}>
{downArrowSymbol} {downArrowSymbol}
</Typography> </Typography>
</Grid> </Paper>
</Grid> </>
); );
} }
@ -154,6 +151,7 @@ const positive = [
"patient", "patient",
"dynamic", "dynamic",
"loyal", "loyal",
"based",
]; ];
const negative = [ const negative = [
@ -177,4 +175,5 @@ const negative = [
"picky", "picky",
"tactless", "tactless",
"thoughtless", "thoughtless",
"cringe",
]; ];

@ -1,21 +1,20 @@
import { Paper, Typography } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import Grid from "@mui/material/Grid"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { IMinigameProps } from "./IMinigameProps"; import { Player } from "../../Player";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { import {
random, downArrowSymbol,
getArrow, getArrow,
getInverseArrow, getInverseArrow,
leftArrowSymbol, leftArrowSymbol,
random,
rightArrowSymbol, rightArrowSymbol,
upArrowSymbol, upArrowSymbol,
downArrowSymbol,
} from "../utils"; } from "../utils";
import { interpolate } from "./Difficulty"; import { interpolate } from "./Difficulty";
import Typography from "@mui/material/Typography"; import { GameTimer } from "./GameTimer";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { IMinigameProps } from "./IMinigameProps";
import { Player } from "../../Player"; import { KeyHandler } from "./KeyHandler";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -55,14 +54,14 @@ export function CheatCodeGame(props: IMinigameProps): React.ReactElement {
} }
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={timer} onExpire={props.onFailure} /> <GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center" }}>
<Typography variant="h4">Enter the Code!</Typography> <Typography variant="h4">Enter the Code!</Typography>
<Typography variant="h4">{code[index]}</Typography> <Typography variant="h4">{code[index]}</Typography>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid> </Paper>
</Grid> </>
); );
} }

@ -1,7 +1,6 @@
import React, { useState, useEffect } from "react"; import { Paper, Typography } from "@mui/material";
import Grid from "@mui/material/Grid"; import React, { useEffect, useState } from "react";
import Typography from "@mui/material/Typography";
interface IProps { interface IProps {
onFinish: () => void; onFinish: () => void;
} }
@ -13,17 +12,13 @@ export function Countdown(props: IProps): React.ReactElement {
props.onFinish(); props.onFinish();
return; return;
} }
setTimeout(() => setX(x - 1), 200); setTimeout(() => setX(x - 1), 300);
}); });
return ( return (
<> <Paper sx={{ p: 1, textAlign: "center" }}>
<Grid container spacing={3}> <Typography variant="h4">Get Ready!</Typography>
<Grid item xs={12}> <Typography variant="h4">{x}</Typography>
<Typography variant="h4">Get Ready!</Typography> </Paper>
<Typography variant="h4">{x}</Typography>
</Grid>
</Grid>
</>
); );
} }

@ -1,15 +1,14 @@
import { Paper, Typography, Box } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import Grid from "@mui/material/Grid";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import { downArrowSymbol, getArrow, leftArrowSymbol, rightArrowSymbol, upArrowSymbol } from "../utils";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
import { Settings } from "../../Settings/Settings";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player"; import { Player } from "../../Player";
import { Settings } from "../../Settings/Settings";
import { KEY } from "../../utils/helpers/keyCodes";
import { downArrowSymbol, getArrow, leftArrowSymbol, rightArrowSymbol, upArrowSymbol } from "../utils";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -19,6 +18,12 @@ interface Difficulty {
symbols: number; symbols: number;
} }
interface GridItem {
content: string;
color: string;
selected?: boolean;
}
const difficulties: { const difficulties: {
Trivial: Difficulty; Trivial: Difficulty;
Normal: Difficulty; Normal: Difficulty;
@ -76,18 +81,33 @@ export function Cyberpunk2077Game(props: IMinigameProps): React.ReactElement {
} }
} }
const flatGrid: GridItem[] = [];
grid.map((line, y) =>
line.map((cell, x) => {
const isCorrectAnswer = cell === answers[currentAnswerIndex];
const optionColor = hasAugment && !isCorrectAnswer ? Settings.theme.disabled : Settings.theme.primary;
if (x === pos[0] && y === pos[1]) {
flatGrid.push({ color: optionColor, content: cell, selected: true });
return;
}
flatGrid.push({ color: optionColor, content: cell });
}),
);
const fontSize = "2em"; const fontSize = "2em";
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={timer} onExpire={props.onFailure} /> <GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center", pb: 1 }}>
<Typography variant="h4">Match the symbols!</Typography> <Typography variant="h4">Match the symbols!</Typography>
<Typography variant="h5" color={Settings.theme.primary}> <Typography variant="h5" color={Settings.theme.primary}>
Targets:{" "} Targets:{" "}
{answers.map((a, i) => { {answers.map((a, i) => {
if (i == currentAnswerIndex) if (i == currentAnswerIndex)
return ( return (
<span key={`${i}`} style={{ fontSize: "1em", color: "blue" }}> <span key={`${i}`} style={{ fontSize: "1em", color: Settings.theme.infolight }}>
{a}&nbsp; {a}&nbsp;
</span> </span>
); );
@ -99,34 +119,30 @@ export function Cyberpunk2077Game(props: IMinigameProps): React.ReactElement {
})} })}
</Typography> </Typography>
<br /> <br />
{grid.map((line, y) => ( <Box
<div key={y}> sx={{
<Typography> display: "grid",
{line.map((cell, x) => { gridTemplateColumns: `repeat(${Math.round(difficulty.width)}, 1fr)`,
const isCorrectAnswer = cell === answers[currentAnswerIndex]; gap: 1,
}}
if (x == pos[0] && y == pos[1]) { >
return ( {flatGrid.map((item) => (
<span key={`${x}${y}`} style={{ fontSize: fontSize, color: "blue" }}> <Typography
{cell}&nbsp; sx={{
</span> fontSize: fontSize,
); color: item.color,
} border: item.selected ? `2px solid ${Settings.theme.infolight}` : "unset",
lineHeight: "unset",
const optionColor = hasAugment && !isCorrectAnswer ? Settings.theme.disabled : Settings.theme.primary; p: item.selected ? "2px" : "4px",
return ( }}
<span key={`${x}${y}`} style={{ fontSize: fontSize, color: optionColor }}> >
{cell}&nbsp; {item.content}
</span>
);
})}
</Typography> </Typography>
<br /> ))}
</div> </Box>
))}
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid> </Paper>
</Grid> </>
); );
} }

@ -1,19 +1,17 @@
import { use } from "../../ui/Context"; import { Button, Container, Paper, Typography } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import Grid from "@mui/material/Grid"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import Button from "@mui/material/Button"; import { use } from "../../ui/Context";
import { Countdown } from "./Countdown";
import { BracketGame } from "./BracketGame";
import { SlashGame } from "./SlashGame";
import { BackwardGame } from "./BackwardGame"; import { BackwardGame } from "./BackwardGame";
import { BracketGame } from "./BracketGame";
import { BribeGame } from "./BribeGame"; import { BribeGame } from "./BribeGame";
import { CheatCodeGame } from "./CheatCodeGame"; import { CheatCodeGame } from "./CheatCodeGame";
import { Countdown } from "./Countdown";
import { Cyberpunk2077Game } from "./Cyberpunk2077Game"; import { Cyberpunk2077Game } from "./Cyberpunk2077Game";
import { MinesweeperGame } from "./MinesweeperGame"; import { MinesweeperGame } from "./MinesweeperGame";
import { WireCuttingGame } from "./WireCuttingGame"; import { SlashGame } from "./SlashGame";
import { Victory } from "./Victory"; import { Victory } from "./Victory";
import Typography from "@mui/material/Typography"; import { WireCuttingGame } from "./WireCuttingGame";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
interface IProps { interface IProps {
StartingDifficulty: number; StartingDifficulty: number;
@ -139,22 +137,20 @@ export function Game(props: IProps): React.ReactElement {
} }
return ( return (
<> <Container>
<Grid container spacing={3}> <Paper sx={{ p: 1, mb: 1, display: "grid", justifyItems: "center", gap: 1 }}>
<Grid item xs={3}> {stage !== Stage.Sell && (
<Button onClick={cancel}>Cancel</Button> <Button sx={{ width: "100%" }} onClick={cancel}>
</Grid> Cancel Infiltration
<Grid item xs={3}> </Button>
<Typography> )}
Level: {level}&nbsp;/&nbsp;{props.MaxLevel} <Typography variant="h5">
</Typography> Level {level} / {props.MaxLevel}
<Progress /> </Typography>
</Grid> <Progress />
</Paper>
<Grid item xs={12}> {stageComponent}
{stageComponent} </Container>
</Grid>
</Grid>
</>
); );
} }

@ -1,4 +1,4 @@
import Grid from "@mui/material/Grid"; import { Paper } from "@mui/material";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
@ -7,6 +7,7 @@ import { ProgressBar } from "../../ui/React/Progress";
interface IProps { interface IProps {
millis: number; millis: number;
onExpire: () => void; onExpire: () => void;
noPaper?: boolean;
} }
export function GameTimer(props: IProps): React.ReactElement { export function GameTimer(props: IProps): React.ReactElement {
@ -30,9 +31,11 @@ export function GameTimer(props: IProps): React.ReactElement {
// https://stackoverflow.com/questions/55593367/disable-material-uis-linearprogress-animation // https://stackoverflow.com/questions/55593367/disable-material-uis-linearprogress-animation
// TODO(hydroflame): there's like a bug where it triggers the end before the // TODO(hydroflame): there's like a bug where it triggers the end before the
// bar physically reaches the end // bar physically reaches the end
return ( return props.noPaper ? (
<Grid item xs={12}> <ProgressBar variant="determinate" value={v} color="primary" />
) : (
<Paper sx={{ p: 1, mb: 1 }}>
<ProgressBar variant="determinate" value={v} color="primary" /> <ProgressBar variant="determinate" value={v} color="primary" />
</Grid> </Paper>
); );
} }

@ -1,9 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Intro } from "./Intro";
import { Game } from "./Game";
import { Location } from "../../Locations/Location"; import { Location } from "../../Locations/Location";
import { use } from "../../ui/Context"; import { use } from "../../ui/Context";
import { calculateDifficulty, calculateReward } from "../formulas/game"; import { calculateDifficulty, calculateReward } from "../formulas/game";
import { Game } from "./Game";
import { Intro } from "./Intro";
interface IProps { interface IProps {
location: Location; location: Location;
} }
@ -22,24 +22,24 @@ export function InfiltrationRoot(props: IProps): React.ReactElement {
router.toCity(); router.toCity();
} }
if (!start) {
return (
<Intro
Location={props.location}
Difficulty={difficulty}
MaxLevel={props.location.infiltrationData.maxClearanceLevel}
start={() => setStart(true)}
cancel={cancel}
/>
);
}
return ( return (
<Game <div style={{ display: "flex", alignItems: "center", height: "calc(100vh - 16px)" }}>
StartingDifficulty={startingSecurityLevel} {start ? (
Difficulty={difficulty} <Game
Reward={reward} StartingDifficulty={startingSecurityLevel}
MaxLevel={props.location.infiltrationData.maxClearanceLevel} Difficulty={difficulty}
/> Reward={reward}
MaxLevel={props.location.infiltrationData.maxClearanceLevel}
/>
) : (
<Intro
Location={props.location}
Difficulty={difficulty}
MaxLevel={props.location.infiltrationData.maxClearanceLevel}
start={() => setStart(true)}
cancel={cancel}
/>
)}
</div>
); );
} }

@ -1,8 +1,8 @@
import { Report } from "@mui/icons-material";
import { Box, Button, Container, Paper, Tooltip, Typography } from "@mui/material";
import React from "react"; import React from "react";
import { Location } from "../../Locations/Location"; import { Location } from "../../Locations/Location";
import Grid from "@mui/material/Grid"; import { Settings } from "../../Settings/Settings";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { numeralWrapper } from "../../ui/numeralFormat"; import { numeralWrapper } from "../../ui/numeralFormat";
interface IProps { interface IProps {
@ -41,9 +41,9 @@ function coloredArrow(difficulty: number): JSX.Element {
} else { } else {
return ( return (
<> <>
{arrowPart("white", difficulty * 13)} {arrowPart(Settings.theme.primary, difficulty * 13)}
{arrowPart("orange", (difficulty - 1) * 13)} {arrowPart(Settings.theme.warning, (difficulty - 1) * 13)}
{arrowPart("red", (difficulty - 2) * 13)} {arrowPart(Settings.theme.error, (difficulty - 2) * 13)}
</> </>
); );
} }
@ -51,65 +51,84 @@ function coloredArrow(difficulty: number): JSX.Element {
export function Intro(props: IProps): React.ReactElement { export function Intro(props: IProps): React.ReactElement {
return ( return (
<> <Container sx={{ alignItems: "center" }}>
<Grid container spacing={3}> <Paper sx={{ p: 1, mb: 1, display: "grid", justifyItems: "center" }}>
<Grid item xs={10}> <Typography variant="h4">
<Typography variant="h4">Infiltrating {props.Location.name}</Typography> Infiltrating <b>{props.Location.name}</b>
</Grid> </Typography>
<Grid item xs={10}> <Typography variant="h6">
<Typography variant="h5" color="primary"> <b>Maximum Level: </b>
Maximum level: {props.MaxLevel} {props.MaxLevel}
</Typography> </Typography>
</Grid> <Typography
<Grid item xs={10}> variant="h6"
<Typography variant="h5" color="primary"> sx={{
Difficulty: {numeralWrapper.format(props.Difficulty * 33.3333, "0")} / 100 color:
</Typography> props.Difficulty > 2
</Grid> ? Settings.theme.error
: props.Difficulty > 1
? Settings.theme.warning
: Settings.theme.primary,
display: "flex",
alignItems: "center",
}}
>
<b>Difficulty:&nbsp;</b>
{numeralWrapper.format(props.Difficulty * 33.3333, "0")} / 100
{props.Difficulty > 1.5 && (
<Tooltip
title={
<Typography color="error">
This location is too heavily guarded for your current stats. It is recommended that you try training,
or finding an easier location.
</Typography>
}
>
<Report sx={{ ml: 1 }} />
</Tooltip>
)}
</Typography>
{props.Difficulty > 1.5 && ( <Typography sx={{ lineHeight: "1em", whiteSpace: "pre" }}>[{coloredArrow(props.Difficulty)}]</Typography>
<Grid item xs={10}> <Typography
<Typography variant="h5" color="primary"> sx={{ lineHeight: "1em", whiteSpace: "pre" }}
Warning: This location is too heavily guarded for your current stats, try training or finding an easier >{`▲ ▲ ▲ ▲`}</Typography>
location. <Typography
</Typography> sx={{ lineHeight: "1em", whiteSpace: "pre" }}
</Grid> >{` Trivial Normal Hard Impossible`}</Typography>
)} </Paper>
<Grid item xs={10}> <Paper sx={{ p: 1, display: "grid", justifyItems: "center" }}>
<Typography sx={{ lineHeight: "1em", whiteSpace: "pre" }}>[{coloredArrow(props.Difficulty)}]</Typography> <Typography sx={{ width: "75%", textAlign: "center" }}>
<Typography <b>Infiltration</b> is a series of short minigames that get progressively harder. You take damage for failing
sx={{ lineHeight: "1em", whiteSpace: "pre" }} them. Reaching the maximum level rewards you with intel that you can trade for money or reputation.
>{` ^ ^ ^ ^`}</Typography> <br />
<Typography <br />
sx={{ lineHeight: "1em", whiteSpace: "pre" }} <b>Gameplay:</b>
>{` Trivial Normal Hard Impossible`}</Typography> </Typography>
</Grid> <ul>
<Grid item xs={10}>
<Typography> <Typography>
Infiltration is a series of short minigames that get progressively harder. You take damage for failing them. <li>
Reaching the maximum level rewards you with intel you can trade for money or reputation. The minigames you play are randomly selected.
<br />
It might take you a few tries to get used to them.
</li>
<li>No game requires use of the mouse.</li>
<li>
<b>Spacebar</b> is the default action/confirm button.
</li>
<li>
The <b>arrow keys</b> and <b>WASD</b> can be used interchangeably.
</li>
<li>Sometimes the rest of the keyboard is used.</li>
</Typography> </Typography>
<br /> </ul>
<Typography>
The minigames you play are randomly selected. It might take you few tries to get used to them. <Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", width: "100%" }}>
</Typography>
<br />
<Typography>No game require use of the mouse.</Typography>
<br />
<Typography>Spacebar is the default action/confirm button.</Typography>
<br />
<Typography>Everything that uses arrow can also use WASD</Typography>
<br />
<Typography>Sometimes the rest of the keyboard is used.</Typography>
</Grid>
<Grid item xs={3}>
<Button onClick={props.start}>Start</Button> <Button onClick={props.start}>Start</Button>
</Grid>
<Grid item xs={3}>
<Button onClick={props.cancel}>Cancel</Button> <Button onClick={props.cancel}>Cancel</Button>
</Grid> </Box>
</Grid> </Paper>
</> </Container>
); );
} }

@ -1,14 +1,16 @@
import React, { useState, useEffect } from "react"; import { Close, Flag, Report } from "@mui/icons-material";
import Grid from "@mui/material/Grid"; import { Box, Paper, Typography } from "@mui/material";
import { IMinigameProps } from "./IMinigameProps"; import { uniqueId } from "lodash";
import { KeyHandler } from "./KeyHandler"; import React, { useEffect, useState } from "react";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import { downArrowSymbol, getArrow, leftArrowSymbol, rightArrowSymbol, upArrowSymbol } from "../utils";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player"; import { Player } from "../../Player";
import { Settings } from "../../Settings/Settings";
import { KEY } from "../../utils/helpers/keyCodes";
import { downArrowSymbol, getArrow, leftArrowSymbol, rightArrowSymbol, upArrowSymbol } from "../utils";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -81,32 +83,77 @@ export function MinesweeperGame(props: IMinigameProps): React.ReactElement {
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
const flatGrid: { flagged?: boolean; current?: boolean; marked?: boolean }[] = [];
minefield.map((line, y) =>
line.map((cell, x) => {
if (memoryPhase) {
flatGrid.push({ flagged: Boolean(minefield[y][x]) });
return;
} else if (x === pos[0] && y === pos[1]) {
flatGrid.push({ current: true });
} else if (answer[y][x]) {
flatGrid.push({ marked: true });
} else if (hasAugment && minefield[y][x]) {
flatGrid.push({ flagged: true });
} else {
flatGrid.push({});
}
}),
);
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={timer} onExpire={props.onFailure} /> <GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center", pb: 1 }}>
<Typography variant="h4">{memoryPhase ? "Remember all the mines!" : "Mark all the mines!"}</Typography> <Typography variant="h4">{memoryPhase ? "Remember all the mines!" : "Mark all the mines!"}</Typography>
{minefield.map((line, y) => ( <Box
<div key={y}> sx={{
<Typography> display: "grid",
{line.map((cell, x) => { gridTemplateColumns: `repeat(${Math.round(difficulty.width)}, 1fr)`,
if (memoryPhase) { gridTemplateRows: `repeat(${Math.round(difficulty.height)}, 1fr)`,
if (minefield[y][x]) return <span key={x}>[?]&nbsp;</span>; gap: 1,
return <span key={x}>[&nbsp;]&nbsp;</span>; }}
} else { >
if (x == pos[0] && y == pos[1]) return <span key={x}>[X]&nbsp;</span>; {flatGrid.map((item) => {
if (answer[y][x]) return <span key={x}>[.]&nbsp;</span>; let color: string;
if (hasAugment && minefield[y][x]) return <span key={x}>[?]&nbsp;</span>; let icon: React.ReactElement;
return <span key={x}>[&nbsp;]&nbsp;</span>;
} if (item.marked) {
})} color = Settings.theme.warning;
</Typography> icon = <Flag />;
<br /> } else if (item.current) {
</div> color = Settings.theme.infolight;
))} icon = <Close />;
} else if (item.flagged) {
color = Settings.theme.error;
icon = <Report />;
} else {
color = Settings.theme.primary;
icon = <></>;
}
return (
<Typography
key={`${item}${uniqueId()}`}
sx={{
color: color,
border: `2px solid ${item.current ? Settings.theme.infolight : Settings.theme.primary}`,
height: "32px",
width: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{icon}
</Typography>
);
})}
</Box>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid> </Paper>
</Grid> </>
); );
} }

@ -1,13 +1,12 @@
import React, { useState, useEffect } from "react"; import { Box, Paper, Typography } from "@mui/material";
import Grid from "@mui/material/Grid"; import React, { useEffect, useState } from "react";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player";
import { KEY } from "../../utils/helpers/keyCodes";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps"; import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler"; import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { interpolate } from "./Difficulty";
import Typography from "@mui/material/Typography";
import { KEY } from "../../utils/helpers/keyCodes";
import { Player } from "../../Player";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -59,23 +58,25 @@ export function SlashGame(props: IMinigameProps): React.ReactElement {
}, []); }, []);
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={5000} onExpire={props.onFailure} /> <GameTimer millis={5000} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center" }}>
<Typography variant="h4">Slash when his guard is down!</Typography> <Typography variant="h4">Slash when his guard is down!</Typography>
{hasAugment ? ( {hasAugment ? (
<> <Box sx={{ my: 1 }}>
<Typography variant="h4">Guard will drop in...</Typography> <Typography variant="h5">Guard will drop in...</Typography>
<GameTimer millis={timeUntilAttacking} onExpire={props.onFailure} /> <GameTimer millis={timeUntilAttacking} onExpire={() => null} noPaper />
</> </Box>
) : ( ) : (
<></> <></>
)} )}
{phase === 0 && <Typography variant="h4">Guarding ...</Typography>} {phase === 0 && <Typography variant="h4">Guarding ...</Typography>}
{phase === 1 && <Typography variant="h4">Preparing?</Typography>} {phase === 1 && <Typography variant="h4">Preparing?</Typography>}
{phase === 2 && <Typography variant="h4">ATTACKING!</Typography>} {phase === 2 && <Typography variant="h4">ATTACKING!</Typography>}
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid> </Paper>
</Grid> </>
); );
} }

@ -1,21 +1,17 @@
import { Factions } from "../../Faction/Factions"; import { Box, Button, MenuItem, Paper, Select, SelectChangeEvent, Typography } from "@mui/material";
import React, { useState } from "react"; import React, { useState } from "react";
import Grid from "@mui/material/Grid"; import { FactionNames } from "../../Faction/data/FactionNames";
import { inviteToFaction } from "../../Faction/FactionHelpers";
import { Factions } from "../../Faction/Factions";
import { use } from "../../ui/Context";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { Reputation } from "../../ui/React/Reputation"; import { Reputation } from "../../ui/React/Reputation";
import { use } from "../../ui/Context";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import MenuItem from "@mui/material/MenuItem";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import { FactionNames } from "../../Faction/data/FactionNames";
import { formatNumber } from "../../utils/StringHelperFunctions"; import { formatNumber } from "../../utils/StringHelperFunctions";
import { import {
calculateInfiltratorsRepReward, calculateInfiltratorsRepReward,
calculateSellInformationCashReward, calculateSellInformationCashReward,
calculateTradeInformationRepReward, calculateTradeInformationRepReward,
} from "../formulas/victory"; } from "../formulas/victory";
import { inviteToFaction } from "../../Faction/FactionHelpers";
interface IProps { interface IProps {
StartingDifficulty: number; StartingDifficulty: number;
@ -66,24 +62,22 @@ export function Victory(props: IProps): React.ReactElement {
} }
return ( return (
<> <Paper sx={{ p: 1, textAlign: "center", display: "flex", alignItems: "center", flexDirection: "column" }}>
<Grid container spacing={3}> <Typography variant="h4">Infiltration successful!</Typography>
<Grid item xs={10}> <Typography variant="h5" color="primary" width="75%">
<Typography variant="h4">Infiltration successful!</Typography> You{" "}
</Grid> {isMemberOfInfiltrators ? (
<Grid item xs={10}> <>
<Typography variant="h5" color="primary"> have gained {formatNumber(infiltrationRepGain, 2)} rep for {FactionNames.ShadowsOfAnarchy} and{" "}
You{" "} </>
{isMemberOfInfiltrators ? ( ) : (
<> <></>
have gained {formatNumber(infiltrationRepGain, 2)} rep for {FactionNames.ShadowsOfAnarchy} and{" "} )}
</> can trade the confidential information you found for money or reputation.
) : ( </Typography>
<></> <Box sx={{ width: "fit-content" }}>
)} <Box sx={{ width: "100%" }}>
can trade the confidential information you found for money or reputation. <Select value={faction} onChange={changeDropdown} sx={{ mr: 1 }}>
</Typography>
<Select value={faction} onChange={changeDropdown}>
<MenuItem key={"none"} value={"none"}> <MenuItem key={"none"} value={"none"}>
{"none"} {"none"}
</MenuItem> </MenuItem>
@ -98,17 +92,15 @@ export function Victory(props: IProps): React.ReactElement {
<Button onClick={trade}> <Button onClick={trade}>
Trade for <Reputation reputation={repGain} /> reputation Trade for <Reputation reputation={repGain} /> reputation
</Button> </Button>
</Grid> </Box>
<Grid item xs={3}> <Button onClick={sell} sx={{ width: "100%" }}>
<Button onClick={sell}> Sell for&nbsp;
Sell for&nbsp; <Money money={moneyGain} />
<Money money={moneyGain} /> </Button>
</Button> </Box>
</Grid> <Button onClick={quitInfiltration} sx={{ width: "100%", mt: 1 }}>
<Grid item xs={3}> Quit
<Button onClick={quitInfiltration}>Quit</Button> </Button>
</Grid> </Paper>
</Grid>
</>
); );
} }

@ -1,15 +1,14 @@
import { Box, Paper, Typography } from "@mui/material";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { random } from "../utils";
import { interpolate } from "./Difficulty";
import { KEY } from "../../utils/helpers/keyCodes";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player"; import { Player } from "../../Player";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { KEY } from "../../utils/helpers/keyCodes";
import { random } from "../utils";
import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
interface Difficulty { interface Difficulty {
[key: string]: number; [key: string]: number;
@ -102,46 +101,53 @@ export function WireCuttingGame(props: IMinigameProps): React.ReactElement {
} }
return ( return (
<Grid container spacing={3}> <>
<GameTimer millis={timer} onExpire={props.onFailure} /> <GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}> <Paper sx={{ display: "grid", justifyItems: "center", pb: 1 }}>
<Typography variant="h4">Cut the wires with the following properties! (keyboard 1 to 9)</Typography> <Typography variant="h4" sx={{ width: "75%", textAlign: "center" }}>
Cut the wires with the following properties! (keyboard 1 to 9)
</Typography>
{questions.map((question, i) => ( {questions.map((question, i) => (
<Typography key={i}>{question.toString()}</Typography> <Typography key={i}>{question.toString()}</Typography>
))} ))}
<Typography> <Box
sx={{
display: "grid",
gridTemplateColumns: `repeat(${wires.length}, 1fr)`,
columnGap: 3,
justifyItems: "center",
}}
>
{new Array(wires.length).fill(0).map((_, i) => { {new Array(wires.length).fill(0).map((_, i) => {
const isCorrectWire = checkWire(i + 1); const isCorrectWire = checkWire(i + 1);
const color = hasAugment && !isCorrectWire ? Settings.theme.disabled : Settings.theme.primary; const color = hasAugment && !isCorrectWire ? Settings.theme.disabled : Settings.theme.primary;
return ( return (
<span key={i} style={{ color: color }}> <Typography key={i} style={{ color: color }}>
&nbsp;{i + 1}&nbsp;&nbsp;&nbsp;&nbsp; {i + 1}
</span> </Typography>
); );
})} })}
</Typography> {new Array(8).fill(0).map((_, i) => (
{new Array(8).fill(0).map((_, i) => ( <React.Fragment key={i}>
<div key={i}>
<Typography>
{wires.map((wire, j) => { {wires.map((wire, j) => {
if ((i === 3 || i === 4) && cutWires[j]) { if ((i === 3 || i === 4) && cutWires[j]) {
return <span key={j}>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>; return <Typography key={j}></Typography>;
} }
const isCorrectWire = checkWire(j + 1); const isCorrectWire = checkWire(j + 1);
const wireColor = const wireColor =
hasAugment && !isCorrectWire ? Settings.theme.disabled : wire.colors[i % wire.colors.length]; hasAugment && !isCorrectWire ? Settings.theme.disabled : wire.colors[i % wire.colors.length];
return ( return (
<span key={j} style={{ color: wireColor }}> <Typography key={j} style={{ color: wireColor }}>
|{wire.tpe}|&nbsp;&nbsp;&nbsp; |{wire.tpe}|
</span> </Typography>
); );
})} })}
</Typography> </React.Fragment>
</div> ))}
))} </Box>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} /> <KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid> </Paper>
</Grid> </>
); );
} }

@ -7,6 +7,7 @@ import { ITutorialEvents } from "./ui/InteractiveTutorial/ITutorialEvents";
// Ordered array of keys to Interactive Tutorial Steps // Ordered array of keys to Interactive Tutorial Steps
enum iTutorialSteps { enum iTutorialSteps {
Start, Start,
NSSelection,
GoToCharacterPage, // Click on 'Stats' page GoToCharacterPage, // Click on 'Stats' page
CharacterPage, // Introduction to 'Stats' page CharacterPage, // Introduction to 'Stats' page
CharacterGoToTerminalPage, // Go back to Terminal CharacterGoToTerminalPage, // Go back to Terminal
@ -43,6 +44,7 @@ const ITutorial: {
isRunning: boolean; isRunning: boolean;
stepIsDone: { stepIsDone: {
[iTutorialSteps.Start]: boolean; [iTutorialSteps.Start]: boolean;
[iTutorialSteps.NSSelection]: boolean;
[iTutorialSteps.GoToCharacterPage]: boolean; [iTutorialSteps.GoToCharacterPage]: boolean;
[iTutorialSteps.CharacterPage]: boolean; [iTutorialSteps.CharacterPage]: boolean;
[iTutorialSteps.CharacterGoToTerminalPage]: boolean; [iTutorialSteps.CharacterGoToTerminalPage]: boolean;
@ -80,6 +82,7 @@ const ITutorial: {
// Keeps track of whether each step has been done // Keeps track of whether each step has been done
stepIsDone: { stepIsDone: {
[iTutorialSteps.Start]: false, [iTutorialSteps.Start]: false,
[iTutorialSteps.NSSelection]: false,
[iTutorialSteps.GoToCharacterPage]: false, [iTutorialSteps.GoToCharacterPage]: false,
[iTutorialSteps.CharacterPage]: false, [iTutorialSteps.CharacterPage]: false,
[iTutorialSteps.CharacterGoToTerminalPage]: false, [iTutorialSteps.CharacterGoToTerminalPage]: false,

@ -156,6 +156,7 @@ const singularity: IMap<any> = {
getUpgradeHomeCoresCost: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 2), getUpgradeHomeCoresCost: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 2),
workForCompany: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost), workForCompany: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost),
applyToCompany: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost), applyToCompany: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost),
quitJob: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost),
getCompanyRep: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 3), getCompanyRep: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 3),
getCompanyFavor: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 3), getCompanyFavor: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 3),
getCompanyFavorGain: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 4), getCompanyFavorGain: SF4Cost(RamCostConstants.ScriptSingularityFn2RamCost / 4),

@ -1233,16 +1233,21 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return false; return false;
} }
}, },
killall: function (_hostname: unknown = workerScript.hostname): boolean { killall: function (_hostname: unknown = workerScript.hostname, _safetyguard: unknown = true): boolean {
updateDynamicRam("killall", getRamCost(Player, "killall")); updateDynamicRam("killall", getRamCost(Player, "killall"));
const hostname = helper.string("killall", "hostname", _hostname); const hostname = helper.string("killall", "hostname", _hostname);
const safetyguard = helper.boolean(_safetyguard);
if (hostname === undefined) { if (hostname === undefined) {
throw makeRuntimeErrorMsg("killall", "Takes 1 argument"); throw makeRuntimeErrorMsg("killall", "Usage: killall(hostname, [safetyguard boolean])");
} }
const server = safeGetServer(hostname, "killall"); const server = safeGetServer(hostname, "killall");
const scriptsRunning = server.runningScripts.length > 0;
let scriptsKilled = 0;
for (let i = server.runningScripts.length - 1; i >= 0; --i) { for (let i = server.runningScripts.length - 1; i >= 0; --i) {
if (safetyguard === true && server.runningScripts[i].pid == workerScript.pid) continue;
killWorkerScript(server.runningScripts[i], server.hostname, false); killWorkerScript(server.runningScripts[i], server.hostname, false);
++scriptsKilled;
} }
WorkerScriptStartStopEventEmitter.emit(); WorkerScriptStartStopEventEmitter.emit();
workerScript.log( workerScript.log(
@ -1250,7 +1255,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
() => `Killing all scripts on '${server.hostname}'. May take a few minutes for the scripts to die.`, () => `Killing all scripts on '${server.hostname}'. May take a few minutes for the scripts to die.`,
); );
return scriptsRunning; return scriptsKilled > 0;
}, },
exit: function (): void { exit: function (): void {
updateDynamicRam("exit", getRamCost(Player, "exit")); updateDynamicRam("exit", getRamCost(Player, "exit"));

@ -295,6 +295,7 @@ export function NetscriptCorporation(
if (office === 0) continue; if (office === 0) continue;
cities.push(office.loc); cities.push(office.loc);
} }
return { return {
name: division.name, name: division.name,
type: division.type, type: division.type,
@ -309,6 +310,7 @@ export function NetscriptCorporation(
upgrades: division.upgrades.slice(), upgrades: division.upgrades.slice(),
cities: cities, cities: cities,
products: division.products === undefined ? [] : Object.keys(division.products), products: division.products === undefined ? [] : Object.keys(division.products),
makesProducts: division.makesProducts,
}; };
} }
@ -359,6 +361,7 @@ export function NetscriptCorporation(
const corporation = getCorporation(); const corporation = getCorporation();
return { return {
cost: material.bCost, cost: material.bCost,
sCost: material.sCost,
name: material.name, name: material.name,
qty: material.qty, qty: material.qty,
qlt: material.qlt, qlt: material.qlt,

@ -342,7 +342,7 @@ export function NetscriptGang(player: IPlayer, workerScript: WorkerScript, helpe
checkGangApiAccess("getBonusTime"); checkGangApiAccess("getBonusTime");
const gang = player.gang; const gang = player.gang;
if (gang === null) throw new Error("Should not be called without Gang"); if (gang === null) throw new Error("Should not be called without Gang");
return Math.round(gang.storedCycles / 5); return Math.round(gang.storedCycles / 5) * 1000;
}, },
}; };
} }

@ -1,4 +1,4 @@
import { Augmentations } from "../Augmentation/Augmentations"; import { StaticAugmentations } from "../Augmentation/StaticAugmentations";
import { hasAugmentationPrereqs } from "../Faction/FactionHelpers"; import { hasAugmentationPrereqs } from "../Faction/FactionHelpers";
import { CityName } from "../Locations/data/CityNames"; import { CityName } from "../Locations/data/CityNames";
import { getRamCost } from "../Netscript/RamCostGenerator"; import { getRamCost } from "../Netscript/RamCostGenerator";
@ -28,10 +28,10 @@ export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, h
updateRam("getAugmentationGraftPrice"); updateRam("getAugmentationGraftPrice");
const augName = helper.string("getAugmentationGraftPrice", "augName", _augName); const augName = helper.string("getAugmentationGraftPrice", "augName", _augName);
checkGraftingAPIAccess("getAugmentationGraftPrice"); checkGraftingAPIAccess("getAugmentationGraftPrice");
if (!getGraftingAvailableAugs(player).includes(augName) || !Augmentations.hasOwnProperty(augName)) { if (!getGraftingAvailableAugs(player).includes(augName) || !StaticAugmentations.hasOwnProperty(augName)) {
throw helper.makeRuntimeErrorMsg("grafting.getAugmentationGraftPrice", `Invalid aug: ${augName}`); throw helper.makeRuntimeErrorMsg("grafting.getAugmentationGraftPrice", `Invalid aug: ${augName}`);
} }
const graftableAug = new GraftableAugmentation(Augmentations[augName]); const graftableAug = new GraftableAugmentation(StaticAugmentations[augName]);
return graftableAug.cost; return graftableAug.cost;
}, },
@ -39,10 +39,10 @@ export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, h
updateRam("getAugmentationGraftTime"); updateRam("getAugmentationGraftTime");
const augName = helper.string("getAugmentationGraftTime", "augName", _augName); const augName = helper.string("getAugmentationGraftTime", "augName", _augName);
checkGraftingAPIAccess("getAugmentationGraftTime"); checkGraftingAPIAccess("getAugmentationGraftTime");
if (!getGraftingAvailableAugs(player).includes(augName) || !Augmentations.hasOwnProperty(augName)) { if (!getGraftingAvailableAugs(player).includes(augName) || !StaticAugmentations.hasOwnProperty(augName)) {
throw helper.makeRuntimeErrorMsg("grafting.getAugmentationGraftTime", `Invalid aug: ${augName}`); throw helper.makeRuntimeErrorMsg("grafting.getAugmentationGraftTime", `Invalid aug: ${augName}`);
} }
const graftableAug = new GraftableAugmentation(Augmentations[augName]); const graftableAug = new GraftableAugmentation(StaticAugmentations[augName]);
return calculateGraftingTimeWithBonus(player, graftableAug); return calculateGraftingTimeWithBonus(player, graftableAug);
}, },
@ -64,7 +64,7 @@ export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, h
"You must be in New Tokyo to begin grafting an Augmentation.", "You must be in New Tokyo to begin grafting an Augmentation.",
); );
} }
if (!getGraftingAvailableAugs(player).includes(augName) || !Augmentations.hasOwnProperty(augName)) { if (!getGraftingAvailableAugs(player).includes(augName) || !StaticAugmentations.hasOwnProperty(augName)) {
workerScript.log("grafting.graftAugmentation", () => `Invalid aug: ${augName}`); workerScript.log("grafting.graftAugmentation", () => `Invalid aug: ${augName}`);
return false; return false;
} }
@ -75,7 +75,7 @@ export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, h
workerScript.log("graftAugmentation", () => txt); workerScript.log("graftAugmentation", () => txt);
} }
const craftableAug = new GraftableAugmentation(Augmentations[augName]); const craftableAug = new GraftableAugmentation(StaticAugmentations[augName]);
if (player.money < craftableAug.cost) { if (player.money < craftableAug.cost) {
workerScript.log("grafting.graftAugmentation", () => `You don't have enough money to craft ${augName}`); workerScript.log("grafting.graftAugmentation", () => `You don't have enough money to craft ${augName}`);
return false; return false;

@ -3,7 +3,7 @@ import { IPlayer } from "../PersonObjects/IPlayer";
import { purchaseAugmentation, joinFaction, getFactionAugmentationsFiltered } from "../Faction/FactionHelpers"; import { purchaseAugmentation, joinFaction, getFactionAugmentationsFiltered } from "../Faction/FactionHelpers";
import { startWorkerScript } from "../NetscriptWorker"; import { startWorkerScript } from "../NetscriptWorker";
import { Augmentation } from "../Augmentation/Augmentation"; import { Augmentation } from "../Augmentation/Augmentation";
import { Augmentations } from "../Augmentation/Augmentations"; import { StaticAugmentations } from "../Augmentation/StaticAugmentations";
import { augmentationExists, installAugmentations } from "../Augmentation/AugmentationHelpers"; import { augmentationExists, installAugmentations } from "../Augmentation/AugmentationHelpers";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { killWorkerScript } from "../Netscript/killWorkerScript"; import { killWorkerScript } from "../Netscript/killWorkerScript";
@ -56,7 +56,7 @@ export function NetscriptSingularity(player: IPlayer, workerScript: WorkerScript
throw _ctx.helper.makeRuntimeErrorMsg(`Invalid augmentation: '${name}'`); throw _ctx.helper.makeRuntimeErrorMsg(`Invalid augmentation: '${name}'`);
} }
return Augmentations[name]; return StaticAugmentations[name];
}; };
const getFaction = function (_ctx: NetscriptContext, name: string): Faction { const getFaction = function (_ctx: NetscriptContext, name: string): Faction {
@ -122,7 +122,7 @@ export function NetscriptSingularity(player: IPlayer, workerScript: WorkerScript
_ctx.helper.checkSingularityAccess(); _ctx.helper.checkSingularityAccess();
const augName = _ctx.helper.string("augName", _augName); const augName = _ctx.helper.string("augName", _augName);
const aug = getAugmentation(_ctx, augName); const aug = getAugmentation(_ctx, augName);
return [aug.baseRepRequirement, aug.baseCost]; return [aug.getCost(player).moneyCost, aug.getCost(player).repCost];
}, },
getAugmentationPrereq: (_ctx: NetscriptContext) => getAugmentationPrereq: (_ctx: NetscriptContext) =>
function (_augName: unknown): string[] { function (_augName: unknown): string[] {
@ -136,14 +136,14 @@ export function NetscriptSingularity(player: IPlayer, workerScript: WorkerScript
_ctx.helper.checkSingularityAccess(); _ctx.helper.checkSingularityAccess();
const augName = _ctx.helper.string("augName", _augName); const augName = _ctx.helper.string("augName", _augName);
const aug = getAugmentation(_ctx, augName); const aug = getAugmentation(_ctx, augName);
return aug.baseCost; return aug.getCost(player).moneyCost;
}, },
getAugmentationRepReq: (_ctx: NetscriptContext) => getAugmentationRepReq: (_ctx: NetscriptContext) =>
function (_augName: unknown): number { function (_augName: unknown): number {
_ctx.helper.checkSingularityAccess(); _ctx.helper.checkSingularityAccess();
const augName = _ctx.helper.string("augName", _augName); const augName = _ctx.helper.string("augName", _augName);
const aug = getAugmentation(_ctx, augName); const aug = getAugmentation(_ctx, augName);
return aug.baseRepRequirement; return aug.getCost(player).repCost;
}, },
getAugmentationStats: (_ctx: NetscriptContext) => getAugmentationStats: (_ctx: NetscriptContext) =>
function (_augName: unknown): AugmentationStats { function (_augName: unknown): AugmentationStats {
@ -183,7 +183,7 @@ export function NetscriptSingularity(player: IPlayer, workerScript: WorkerScript
} }
} }
if (fac.playerReputation < aug.baseRepRequirement) { if (fac.playerReputation < aug.getCost(player).repCost) {
_ctx.log(() => `You do not have enough reputation with '${fac.name}'.`); _ctx.log(() => `You do not have enough reputation with '${fac.name}'.`);
return false; return false;
} }
@ -947,6 +947,12 @@ export function NetscriptSingularity(player: IPlayer, workerScript: WorkerScript
} }
return res; return res;
}, },
quitJob: (_ctx: NetscriptContext) =>
function (_companyName: unknown): void {
_ctx.helper.checkSingularityAccess();
const companyName = _ctx.helper.string("companyName", _companyName);
player.quitJob(companyName);
},
getCompanyRep: (_ctx: NetscriptContext) => getCompanyRep: (_ctx: NetscriptContext) =>
function (_companyName: unknown): number { function (_companyName: unknown): number {
_ctx.helper.checkSingularityAccess(); _ctx.helper.checkSingularityAccess();
@ -1079,7 +1085,6 @@ export function NetscriptSingularity(player: IPlayer, workerScript: WorkerScript
_ctx.log(() => `Invalid work type: '${type}`); _ctx.log(() => `Invalid work type: '${type}`);
return false; return false;
} }
return true;
}, },
getFactionRep: (_ctx: NetscriptContext) => getFactionRep: (_ctx: NetscriptContext) =>
function (_facName: unknown): number { function (_facName: unknown): number {

@ -5,7 +5,7 @@ import { FactionWorkType } from "../Faction/FactionWorkTypeEnum";
import { SleeveTaskType } from "../PersonObjects/Sleeve/SleeveTaskTypesEnum"; import { SleeveTaskType } from "../PersonObjects/Sleeve/SleeveTaskTypesEnum";
import { WorkerScript } from "../Netscript/WorkerScript"; import { WorkerScript } from "../Netscript/WorkerScript";
import { findSleevePurchasableAugs } from "../PersonObjects/Sleeve/SleeveHelpers"; import { findSleevePurchasableAugs } from "../PersonObjects/Sleeve/SleeveHelpers";
import { Augmentations } from "../Augmentation/Augmentations"; import { StaticAugmentations } from "../Augmentation/StaticAugmentations";
import { CityName } from "../Locations/data/CityNames"; import { CityName } from "../Locations/data/CityNames";
import { findCrime } from "../Crime/CrimeHelpers"; import { findCrime } from "../Crime/CrimeHelpers";
@ -286,7 +286,7 @@ export function NetscriptSleeve(player: IPlayer, workerScript: WorkerScript, hel
const aug = purchasableAugs[i]; const aug = purchasableAugs[i];
augs.push({ augs.push({
name: aug.name, name: aug.name,
cost: aug.startingCost, cost: aug.baseCost,
}); });
} }
@ -303,7 +303,7 @@ export function NetscriptSleeve(player: IPlayer, workerScript: WorkerScript, hel
throw helper.makeRuntimeErrorMsg("sleeve.purchaseSleeveAug", `Sleeve shock too high: Sleeve ${sleeveNumber}`); throw helper.makeRuntimeErrorMsg("sleeve.purchaseSleeveAug", `Sleeve shock too high: Sleeve ${sleeveNumber}`);
} }
const aug = Augmentations[augName]; const aug = StaticAugmentations[augName];
if (!aug) { if (!aug) {
throw helper.makeRuntimeErrorMsg("sleeve.purchaseSleeveAug", `Invalid aug: ${augName}`); throw helper.makeRuntimeErrorMsg("sleeve.purchaseSleeveAug", `Invalid aug: ${augName}`);
} }

@ -18,7 +18,7 @@ export class GraftableAugmentation {
} }
get cost(): number { get cost(): number {
return this.augmentation.startingCost * CONSTANTS.AugmentationGraftingCostMult; return this.augmentation.baseCost * CONSTANTS.AugmentationGraftingCostMult;
} }
get time(): number { get time(): number {

@ -1,11 +1,11 @@
import { Augmentations } from "../../Augmentation/Augmentations"; import { StaticAugmentations } from "../../Augmentation/StaticAugmentations";
import { GraftableAugmentation } from "./GraftableAugmentation"; import { GraftableAugmentation } from "./GraftableAugmentation";
import { IPlayer } from "../IPlayer"; import { IPlayer } from "../IPlayer";
export const getGraftingAvailableAugs = (player: IPlayer): string[] => { export const getGraftingAvailableAugs = (player: IPlayer): string[] => {
const augs: string[] = []; const augs: string[] = [];
for (const [augName, aug] of Object.entries(Augmentations)) { for (const [augName, aug] of Object.entries(StaticAugmentations)) {
if (aug.isSpecial) continue; if (aug.isSpecial) continue;
augs.push(augName); augs.push(augName);
} }

@ -2,7 +2,7 @@ import { Construction, CheckBox, CheckBoxOutlineBlank } from "@mui/icons-materia
import { Box, Button, Container, List, ListItemButton, Paper, Typography } from "@mui/material"; import { Box, Button, Container, List, ListItemButton, Paper, Typography } from "@mui/material";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Augmentation } from "../../../Augmentation/Augmentation"; import { Augmentation } from "../../../Augmentation/Augmentation";
import { Augmentations } from "../../../Augmentation/Augmentations"; import { StaticAugmentations } from "../../../Augmentation/StaticAugmentations";
import { AugmentationNames } from "../../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../../Augmentation/data/AugmentationNames";
import { CONSTANTS } from "../../../Constants"; import { CONSTANTS } from "../../../Constants";
import { hasAugmentationPrereqs } from "../../../Faction/FactionHelpers"; import { hasAugmentationPrereqs } from "../../../Faction/FactionHelpers";
@ -54,7 +54,7 @@ export const GraftingRoot = (): React.ReactElement => {
const player = use.Player(); const player = use.Player();
const router = use.Router(); const router = use.Router();
for (const aug of Object.values(Augmentations)) { for (const aug of Object.values(StaticAugmentations)) {
const name = aug.name; const name = aug.name;
const graftableAug = new GraftableAugmentation(aug); const graftableAug = new GraftableAugmentation(aug);
GraftableAugmentations[name] = graftableAug; GraftableAugmentations[name] = graftableAug;
@ -62,6 +62,7 @@ export const GraftingRoot = (): React.ReactElement => {
const [selectedAug, setSelectedAug] = useState(getGraftingAvailableAugs(player)[0]); const [selectedAug, setSelectedAug] = useState(getGraftingAvailableAugs(player)[0]);
const [graftOpen, setGraftOpen] = useState(false); const [graftOpen, setGraftOpen] = useState(false);
const selectedAugmentation = StaticAugmentations[selectedAug];
const setRerender = useState(false)[1]; const setRerender = useState(false)[1];
function rerender(): void { function rerender(): void {
@ -148,22 +149,26 @@ export const GraftingRoot = (): React.ReactElement => {
{/* Use formula so the displayed creation time is accurate to player bonus */} {/* Use formula so the displayed creation time is accurate to player bonus */}
</Typography> </Typography>
{Augmentations[selectedAug].prereqs.length > 0 && ( {selectedAugmentation.prereqs.length > 0 && (
<AugPreReqsChecklist player={player} aug={Augmentations[selectedAug]} /> <AugPreReqsChecklist player={player} aug={selectedAugmentation} />
)} )}
<br /> <br />
<Typography> <Typography>
{(() => { {(() => {
const aug = Augmentations[selectedAug]; const info =
typeof selectedAugmentation.info === "string" ? (
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info; <span>{selectedAugmentation.info}</span>
) : (
selectedAugmentation.info
);
const tooltip = ( const tooltip = (
<> <>
{info} {info}
<br /> <br />
<br /> <br />
{aug.stats} {selectedAugmentation.stats}
</> </>
); );
return tooltip; return tooltip;

@ -248,7 +248,7 @@ export interface IPlayer {
queryStatFromString(str: string): number; queryStatFromString(str: string): number;
getIntelligenceBonus(weight: number): number; getIntelligenceBonus(weight: number): number;
getCasinoWinnings(): number; getCasinoWinnings(): number;
quitJob(company: string): void; quitJob(company: string, sing?: boolean): void;
hasJob(): boolean; hasJob(): boolean;
createHacknetServer(): HacknetServer; createHacknetServer(): HacknetServer;
startCreateProgramWork(programName: string, time: number, reqLevel: number): void; startCreateProgramWork(programName: string, time: number, reqLevel: number): void;
@ -291,6 +291,6 @@ export interface IPlayer {
sourceFileLvl(n: number): number; sourceFileLvl(n: number): number;
startGraftAugmentationWork(augmentationName: string, time: number): void; startGraftAugmentationWork(augmentationName: string, time: number): void;
graftAugmentationWork(numCycles: number): boolean; graftAugmentationWork(numCycles: number): boolean;
finishGraftAugmentationWork(cancelled: boolean): string; finishGraftAugmentationWork(cancelled: boolean, singularity?: boolean): string;
applyEntropy(stacks?: number): void; applyEntropy(stacks?: number): void;
} }

@ -258,7 +258,7 @@ export class PlayerObject implements IPlayer {
queryStatFromString: (str: string) => number; queryStatFromString: (str: string) => number;
getIntelligenceBonus: (weight: number) => number; getIntelligenceBonus: (weight: number) => number;
getCasinoWinnings: () => number; getCasinoWinnings: () => number;
quitJob: (company: string) => void; quitJob: (company: string, sing?: boolean) => void;
hasJob: () => boolean; hasJob: () => boolean;
process: (router: IRouter, numCycles?: number) => void; process: (router: IRouter, numCycles?: number) => void;
createHacknetServer: () => HacknetServer; createHacknetServer: () => HacknetServer;
@ -302,7 +302,7 @@ export class PlayerObject implements IPlayer {
sourceFileLvl: (n: number) => number; sourceFileLvl: (n: number) => number;
startGraftAugmentationWork: (augmentationName: string, time: number) => void; startGraftAugmentationWork: (augmentationName: string, time: number) => void;
graftAugmentationWork: (numCycles: number) => boolean; graftAugmentationWork: (numCycles: number) => boolean;
finishGraftAugmentationWork: (cancelled: boolean) => string; finishGraftAugmentationWork: (cancelled: boolean, singularity?: boolean) => string;
applyEntropy: (stacks?: number) => void; applyEntropy: (stacks?: number) => void;
constructor() { constructor() {
@ -463,12 +463,12 @@ export class PlayerObject implements IPlayer {
this.bladeburner = null; this.bladeburner = null;
this.bladeburner_max_stamina_mult = 1; this.bladeburner_max_stamina_mult = 1;
this.bladeburner_stamina_gain_mult = 1; this.bladeburner_stamina_gain_mult = 1;
this.bladeburner_analysis_mult = 1; //Field Analysis Onl; this.bladeburner_analysis_mult = 1; //Field Analysis Only
this.bladeburner_success_chance_mult = 1; this.bladeburner_success_chance_mult = 1;
// Sleeves & Re-sleeving // Sleeves & Re-sleeving
this.sleeves = []; this.sleeves = [];
this.sleevesFromCovenant = 0; // # of Duplicate sleeves purchased from the covenan; this.sleevesFromCovenant = 0; // # of Duplicate sleeves purchased from the covenant
//bitnode //bitnode
this.bitNodeN = 1; this.bitNodeN = 1;
@ -483,8 +483,8 @@ export class PlayerObject implements IPlayer {
this.playtimeSinceLastBitnode = 0; this.playtimeSinceLastBitnode = 0;
// Keep track of where money comes from // Keep track of where money comes from
this.moneySourceA = new MoneySourceTracker(); // Where money comes from since last-installed Augmentatio; this.moneySourceA = new MoneySourceTracker(); // Where money comes from since last-installed Augmentation
this.moneySourceB = new MoneySourceTracker(); // Where money comes from for this entire BitNode ru; this.moneySourceB = new MoneySourceTracker(); // Where money comes from for this entire BitNode run
// Production since last Augmentation installation // Production since last Augmentation installation
this.scriptProdSinceLastAug = 0; this.scriptProdSinceLastAug = 0;

@ -1,6 +1,5 @@
import { IPlayer } from "../IPlayer"; import { IPlayer } from "../IPlayer";
import { PlayerObject } from "./PlayerObject"; import { PlayerObject } from "./PlayerObject";
import { Augmentations } from "../../Augmentation/Augmentations";
import { applyAugmentation } from "../../Augmentation/AugmentationHelpers"; import { applyAugmentation } from "../../Augmentation/AugmentationHelpers";
import { PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation"; import { PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
@ -1364,10 +1363,10 @@ export function craftAugmentationWork(this: IPlayer, numCycles: number): boolean
return false; return false;
} }
export function finishGraftAugmentationWork(this: IPlayer, cancelled: boolean): string { export function finishGraftAugmentationWork(this: IPlayer, cancelled: boolean, singularity = false): string {
const augName = this.graftAugmentationName; const augName = this.graftAugmentationName;
if (cancelled === false) { if (cancelled === false) {
applyAugmentation(Augmentations[augName]); applyAugmentation({ name: augName, level: 1 });
if (!this.hasAugmentation(AugmentationNames.CongruityImplant)) { if (!this.hasAugmentation(AugmentationNames.CongruityImplant)) {
this.entropy += 1; this.entropy += 1;
@ -1378,7 +1377,7 @@ export function finishGraftAugmentationWork(this: IPlayer, cancelled: boolean):
`You've finished grafting ${augName}.<br>The augmentation has been applied to your body` + `You've finished grafting ${augName}.<br>The augmentation has been applied to your body` +
(this.hasAugmentation(AugmentationNames.CongruityImplant) ? "." : ", but you feel a bit off."), (this.hasAugmentation(AugmentationNames.CongruityImplant) ? "." : ", but you feel a bit off."),
); );
} else { } else if (cancelled && singularity === false) {
dialogBoxCreate(`You cancelled the grafting of ${augName}.<br>Your money was not returned to you.`); dialogBoxCreate(`You cancelled the grafting of ${augName}.<br>Your money was not returned to you.`);
} }
@ -1700,6 +1699,9 @@ export function singularityStopWork(this: IPlayer): string {
case CONSTANTS.WorkTypeCrime: case CONSTANTS.WorkTypeCrime:
res = this.finishCrime(true); res = this.finishCrime(true);
break; break;
case CONSTANTS.WorkTypeGraftAugmentation:
res = this.finishGraftAugmentationWork(true, true);
break;
default: default:
console.error(`Unrecognized work type (${this.workType})`); console.error(`Unrecognized work type (${this.workType})`);
return ""; return "";
@ -1748,14 +1750,6 @@ export function hospitalize(this: IPlayer): number {
//The 'sing' argument designates whether or not this is being called from //The 'sing' argument designates whether or not this is being called from
//the applyToCompany() Netscript Singularity function //the applyToCompany() Netscript Singularity function
export function applyForJob(this: IPlayer, entryPosType: CompanyPosition, sing = false): boolean { export function applyForJob(this: IPlayer, entryPosType: CompanyPosition, sing = false): boolean {
// Get current company and job
let currCompany = null;
if (this.companyName !== "") {
currCompany = Companies[this.companyName];
}
const currPositionName = this.jobs[this.companyName];
// Get company that's being applied to
const company = Companies[this.location]; //Company being applied to const company = Companies[this.location]; //Company being applied to
if (!(company instanceof Company)) { if (!(company instanceof Company)) {
console.error(`Could not find company that matches the location: ${this.location}. Player.applyToCompany() failed`); console.error(`Could not find company that matches the location: ${this.location}. Player.applyToCompany() failed`);
@ -1765,66 +1759,44 @@ export function applyForJob(this: IPlayer, entryPosType: CompanyPosition, sing =
let pos = entryPosType; let pos = entryPosType;
if (!this.isQualified(company, pos)) { if (!this.isQualified(company, pos)) {
const reqText = getJobRequirementText(company, pos);
if (!sing) { if (!sing) {
dialogBoxCreate("Unfortunately, you do not qualify for this position<br>" + reqText); dialogBoxCreate("Unfortunately, you do not qualify for this position<br>" + getJobRequirementText(company, pos));
} }
return false; return false;
} }
// Check if this company has the position
if (!company.hasPosition(pos)) { if (!company.hasPosition(pos)) {
console.error(`Company ${company.name} does not have position ${pos}. Player.applyToCompany() failed`);
return false; return false;
} }
while (true) { while (true) {
const newPos = getNextCompanyPositionHelper(pos); const nextPos = getNextCompanyPositionHelper(pos);
if (newPos == null) { if (nextPos == null) break;
break; if (company.hasPosition(nextPos) && this.isQualified(company, nextPos)) {
} pos = nextPos;
} else break;
//Check if this company has this position
if (company.hasPosition(newPos)) {
if (!this.isQualified(company, newPos)) {
//If player not qualified for next job, break loop so player will be given current job
break;
}
pos = newPos;
} else {
break;
}
} }
//Check if the determined job is the same as the player's current job //Check if player already has the assigned job
if (currCompany != null) { if (this.jobs[company.name] === pos.name) {
if (currCompany.name == company.name && pos.name == currPositionName) { if (!sing) {
const nextPos = getNextCompanyPositionHelper(pos); const nextPos = getNextCompanyPositionHelper(pos);
if (nextPos == null) { if (nextPos == null || !company.hasPosition(nextPos)) {
if (!sing) { dialogBoxCreate("You are already at the highest position for your field! No promotion available");
dialogBoxCreate("You are already at the highest position for your field! No promotion available");
}
return false;
} else if (company.hasPosition(nextPos)) {
if (!sing) {
const reqText = getJobRequirementText(company, nextPos);
dialogBoxCreate("Unfortunately, you do not qualify for a promotion<br>" + reqText);
}
return false;
} else { } else {
if (!sing) { const reqText = getJobRequirementText(company, nextPos);
dialogBoxCreate("You are already at the highest position for your field! No promotion available"); dialogBoxCreate("Unfortunately, you do not qualify for a promotion<br>" + reqText);
}
return false;
} }
} }
return false;
} }
this.jobs[company.name] = pos.name; this.jobs[company.name] = pos.name;
if (!this.focus && this.isWorking && this.companyName !== this.location) this.resetWorkStatus(); if (!this.isWorking || !this.workType.includes("Working for Company")) this.companyName = company.name;
this.companyName = this.location;
if (!sing) { if (!sing) {
dialogBoxCreate("Congratulations! You were offered a new job at " + this.companyName + " as a " + pos.name + "!"); dialogBoxCreate("Congratulations! You were offered a new job at " + company.name + " as a " + pos.name + "!");
} }
return true; return true;
} }
@ -1869,7 +1841,7 @@ export function getNextCompanyPosition(
return entryPosType; return entryPosType;
} }
export function quitJob(this: IPlayer, company: string): void { export function quitJob(this: IPlayer, company: string, _sing = false): void {
if (this.isWorking == true && this.workType.includes("Working for Company") && this.companyName == company) { if (this.isWorking == true && this.workType.includes("Working for Company") && this.companyName == company) {
this.finishWork(true); this.finishWork(true);
} }

@ -686,7 +686,7 @@ export class Sleeve extends Person {
} }
tryBuyAugmentation(p: IPlayer, aug: Augmentation): boolean { tryBuyAugmentation(p: IPlayer, aug: Augmentation): boolean {
if (!p.canAfford(aug.startingCost)) { if (!p.canAfford(aug.baseCost)) {
return false; return false;
} }
@ -695,7 +695,7 @@ export class Sleeve extends Person {
return false; return false;
} }
p.loseMoney(aug.startingCost, "sleeves"); p.loseMoney(aug.baseCost, "sleeves");
this.installAugmentation(aug); this.installAugmentation(aug);
return true; return true;
} }

@ -4,7 +4,7 @@ import { Sleeve } from "./Sleeve";
import { IPlayer } from "../IPlayer"; import { IPlayer } from "../IPlayer";
import { Augmentation } from "../../Augmentation/Augmentation"; import { Augmentation } from "../../Augmentation/Augmentation";
import { Augmentations } from "../../Augmentation/Augmentations"; import { StaticAugmentations } from "../../Augmentation/StaticAugmentations";
import { Faction } from "../../Faction/Faction"; import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions"; import { Factions } from "../../Faction/Factions";
@ -64,13 +64,13 @@ export function findSleevePurchasableAugs(sleeve: Sleeve, p: IPlayer): Augmentat
if (p.inGang()) { if (p.inGang()) {
const fac = p.getGangFaction(); const fac = p.getGangFaction();
for (const augName of Object.keys(Augmentations)) { for (const augName of Object.keys(StaticAugmentations)) {
const aug = Augmentations[augName]; const aug = StaticAugmentations[augName];
if (!isAvailableForSleeve(aug)) { if (!isAvailableForSleeve(aug)) {
continue; continue;
} }
if (fac.playerReputation > aug.baseRepRequirement) { if (fac.playerReputation > aug.getCost(p).repCost) {
availableAugs.push(aug); availableAugs.push(aug);
} }
} }
@ -89,12 +89,12 @@ export function findSleevePurchasableAugs(sleeve: Sleeve, p: IPlayer): Augmentat
} }
for (const augName of fac.augmentations) { for (const augName of fac.augmentations) {
const aug: Augmentation = Augmentations[augName]; const aug: Augmentation = StaticAugmentations[augName];
if (!isAvailableForSleeve(aug)) { if (!isAvailableForSleeve(aug)) {
continue; continue;
} }
if (fac.playerReputation > aug.baseRepRequirement) { if (fac.playerReputation > aug.getCost(p).repCost) {
availableAugs.push(aug); availableAugs.push(aug);
} }
} }

@ -52,7 +52,7 @@ export function SleeveAugmentationsModal(props: IProps): React.ReactElement {
ownedAugNames={ownedAugNames} ownedAugNames={ownedAugNames}
player={player} player={player}
canPurchase={(player, aug) => { canPurchase={(player, aug) => {
return player.money > aug.startingCost; return player.money > aug.baseCost;
}} }}
purchaseAugmentation={(player, aug, _showModal) => { purchaseAugmentation={(player, aug, _showModal) => {
props.sleeve.tryBuyAugmentation(player, aug); props.sleeve.tryBuyAugmentation(player, aug);

@ -77,7 +77,9 @@ function possibleFactions(player: IPlayer, sleeve: Sleeve): string[] {
} }
return factions.filter((faction) => { return factions.filter((faction) => {
const facInfo = Factions[faction].getInfo(); const factionObj = Factions[faction];
if (!factionObj) return false;
const facInfo = factionObj.getInfo();
return facInfo.offerHackingWork || facInfo.offerFieldWork || facInfo.offerSecurityWork; return facInfo.offerHackingWork || facInfo.offerFieldWork || facInfo.offerSecurityWork;
}); });
} }

@ -1,6 +1,6 @@
import { FactionNames } from "./Faction/data/FactionNames"; import { FactionNames } from "./Faction/data/FactionNames";
import { CityName } from "./Locations/data/CityNames"; import { CityName } from "./Locations/data/CityNames";
import { Augmentations } from "./Augmentation/Augmentations"; import { StaticAugmentations } from "./Augmentation/StaticAugmentations";
import { augmentationExists, initAugmentations } from "./Augmentation/AugmentationHelpers"; import { augmentationExists, initAugmentations } from "./Augmentation/AugmentationHelpers";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames"; import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { initBitNodeMultipliers } from "./BitNode/BitNode"; import { initBitNodeMultipliers } from "./BitNode/BitNode";
@ -225,9 +225,9 @@ export function prestigeSourceFile(flume: boolean): void {
} }
// Delete all Augmentations // Delete all Augmentations
for (const name of Object.keys(Augmentations)) { for (const name of Object.keys(StaticAugmentations)) {
if (Augmentations.hasOwnProperty(name)) { if (StaticAugmentations.hasOwnProperty(name)) {
delete Augmentations[name]; delete StaticAugmentations[name];
} }
} }

@ -1800,6 +1800,18 @@ export interface Singularity {
*/ */
workForCompany(companyName?: string, focus?: boolean): boolean; workForCompany(companyName?: string, focus?: boolean): boolean;
/**
* Quit jobs by company.
* @remarks
* RAM cost: 3 GB * 16/4/1
*
*
* This function will finish work with the company provided and quit any jobs.
*
* @param companyName - Name of the company.
*/
quitJob(companyName?: string): void;
/** /**
* Apply for a job at a company. * Apply for a job at a company.
* @remarks * @remarks
@ -2340,13 +2352,13 @@ export interface Singularity {
* @example * @example
* ```ts * ```ts
* // NS1 * // NS1
* getDarkwebProgramsAvailable(); * getDarkwebPrograms();
* // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc] * // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc]
* ``` * ```
* @example * @example
* ```ts * ```ts
* // NS2 * // NS2
* ns.getDarkwebProgramsAvailable(); * ns.getDarkwebPrograms();
* // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc] * // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc]
* ``` * ```
* @returns - a list of programs available for purchase on the dark web, or [] if Tor has not * @returns - a list of programs available for purchase on the dark web, or [] if Tor has not
@ -5311,9 +5323,10 @@ export interface NS {
* If no host is defined, it will kill all scripts, where the script is running. * If no host is defined, it will kill all scripts, where the script is running.
* *
* @param host - IP or hostname of the server on which to kill all scripts. * @param host - IP or hostname of the server on which to kill all scripts.
* @param safetyguard - Skips the script that calls this function
* @returns True if any scripts were killed, and false otherwise. * @returns True if any scripts were killed, and false otherwise.
*/ */
killall(host?: string): boolean; killall(host?: string, safetyguard?: boolean): boolean;
/** /**
* Terminates the current script immediately. * Terminates the current script immediately.
@ -6126,7 +6139,7 @@ export interface NS {
* Returns 0 if the script does not exist. * Returns 0 if the script does not exist.
* *
* @param script - Filename of script. This is case-sensitive. * @param script - Filename of script. This is case-sensitive.
* @param host - Host of target server the script is located on. This is optional, If it is not specified then the function will se the current server as the target server. * @param host - Host of target server the script is located on. This is optional, if it is not specified then the function will use the current server as the target server.
* @returns Amount of RAM (in GB) required to run the specified script on the target server, and 0 if the script does not exist. * @returns Amount of RAM (in GB) required to run the specified script on the target server, and 0 if the script does not exist.
*/ */
getScriptRam(script: string, host?: string): number; getScriptRam(script: string, host?: string): number;
@ -7044,22 +7057,27 @@ interface CorporationInfo {
interface Employee { interface Employee {
/** Name of the employee */ /** Name of the employee */
name: string; name: string;
/** Morale */ /** Morale of the employee */
mor: number; mor: number;
/** Happiness */ /** Happiness of the employee */
hap: number; hap: number;
/** Energy */ /** Energy of the employee */
ene: number; ene: number;
/** Intelligence of the employee */
int: number; int: number;
/** Charisma of the employee */
cha: number; cha: number;
/** Experience of the employee */
exp: number; exp: number;
/** Creativity of the employee */
cre: number; cre: number;
/** Efficiency of the employee */
eff: number; eff: number;
/** Salary */ /** Salary of the employee */
sal: number; sal: number;
/** City */ /** Current Location (city) */
loc: string; loc: string;
/** Current job */ /** Current job position */
pos: string; pos: string;
} }
@ -7111,6 +7129,8 @@ interface Material {
sell: number; sell: number;
/** cost to buy material */ /** cost to buy material */
cost: number; cost: number;
/** Sell cost, can be "MP+5" */
sCost: string | number;
} }
/** /**
@ -7202,6 +7222,8 @@ interface Division {
cities: string[]; cities: string[];
/** Products developed by this division */ /** Products developed by this division */
products: string[]; products: string[];
/** Whether the industry this division is in is capable of making products */
makesProducts: boolean;
} }
/** /**

@ -544,11 +544,14 @@ export function Root(props: IProps): React.ReactElement {
// this is duplicate code with saving later. // this is duplicate code with saving later.
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) { if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
//Make sure filename + code properly follow tutorial //Make sure filename + code properly follow tutorial
if (currentScript.fileName !== "n00dles.script") { if (currentScript.fileName !== "n00dles.script" && currentScript.fileName !== "n00dles.js") {
dialogBoxCreate("Leave the script name as 'n00dles.script'!"); dialogBoxCreate("Don't change the script name for now.");
return; return;
} }
if (currentScript.code.replace(/\s/g, "").indexOf("while(true){hack('n00dles');}") == -1) { const cleanCode = currentScript.code.replace(/\s/g, "");
const ns1 = "while(true){hack('n00dles');}";
const ns2 = `exportasyncfunctionmain(ns){while(true){awaitns.hack('n00dles');}}`;
if (cleanCode.indexOf(ns1) == -1 && cleanCode.indexOf(ns2) == -1) {
dialogBoxCreate("Please copy and paste the code from the tutorial!"); dialogBoxCreate("Please copy and paste the code from the tutorial!");
return; return;
} }
@ -692,7 +695,7 @@ export function Root(props: IProps): React.ReactElement {
const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName); const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName);
if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex as number].code) { if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex as number].code) {
PromptEvent.emit({ PromptEvent.emit({
txt: "Do you want to save changes to " + closingScript.fileName + "?", txt: `Do you want to save changes to ${closingScript.fileName} on ${closingScript.hostname}?`,
resolve: (result: boolean | string) => { resolve: (result: boolean | string) => {
if (result) { if (result) {
// Save changes // Save changes
@ -855,22 +858,31 @@ export function Root(props: IProps): React.ReactElement {
)} )}
</Tooltip> </Tooltip>
{filteredOpenScripts.map(({ fileName, hostname }, index) => { {filteredOpenScripts.map(({ fileName, hostname }, index) => {
const editingCurrentScript =
currentScript?.fileName === filteredOpenScripts[index].fileName &&
currentScript?.hostname === filteredOpenScripts[index].hostname;
const externalScript = hostname !== "home";
const colorProps = editingCurrentScript
? {
background: Settings.theme.button,
borderColor: Settings.theme.button,
color: Settings.theme.primary,
}
: {
background: Settings.theme.backgroundsecondary,
borderColor: Settings.theme.backgroundsecondary,
color: Settings.theme.secondary,
};
if (externalScript) {
colorProps.color = Settings.theme.info;
}
const iconButtonStyle = { const iconButtonStyle = {
maxWidth: `${tabIconWidth}px`, maxWidth: `${tabIconWidth}px`,
minWidth: `${tabIconWidth}px`, minWidth: `${tabIconWidth}px`,
minHeight: "38.5px", minHeight: "38.5px",
maxHeight: "38.5px", maxHeight: "38.5px",
...(currentScript?.fileName === filteredOpenScripts[index].fileName ...colorProps,
? {
background: Settings.theme.button,
borderColor: Settings.theme.button,
color: Settings.theme.primary,
}
: {
background: Settings.theme.backgroundsecondary,
borderColor: Settings.theme.backgroundsecondary,
color: Settings.theme.secondary,
}),
}; };
const scriptTabText = `${hostname}:~/${fileName} ${dirty(index)}`; const scriptTabText = `${hostname}:~/${fileName} ${dirty(index)}`;
@ -905,17 +917,7 @@ export function Root(props: IProps): React.ReactElement {
maxWidth: `${tabTextWidth}px`, maxWidth: `${tabTextWidth}px`,
minHeight: "38.5px", minHeight: "38.5px",
overflow: "hidden", overflow: "hidden",
...(currentScript?.fileName === filteredOpenScripts[index].fileName ...colorProps,
? {
background: Settings.theme.button,
borderColor: Settings.theme.button,
color: Settings.theme.primary,
}
: {
background: Settings.theme.backgroundsecondary,
borderColor: Settings.theme.backgroundsecondary,
color: Settings.theme.secondary,
}),
}} }}
> >
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}> <span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { KEY } from "../../utils/helpers/keyCodes"; import { KEYCODE } from "../../utils/helpers/keyCodes";
import clsx from "clsx"; import clsx from "clsx";
import { styled, Theme, CSSObject } from "@mui/material/styles"; import { styled, Theme, CSSObject } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
@ -275,57 +275,53 @@ export function SidebarRoot(props: IProps): React.ReactElement {
function handleShortcuts(this: Document, event: KeyboardEvent): any { function handleShortcuts(this: Document, event: KeyboardEvent): any {
if (Settings.DisableHotkeys) return; if (Settings.DisableHotkeys) return;
if ((props.player.isWorking && props.player.focus) || props.router.page() === Page.BitVerse) return; if ((props.player.isWorking && props.player.focus) || props.router.page() === Page.BitVerse) return;
if (event.key === KEY.T && event.altKey) { if (event.code === KEYCODE.T && event.altKey) {
event.preventDefault(); event.preventDefault();
clickTerminal(); clickTerminal();
} else if (event.key === KEY.C && event.altKey) { } else if (event.code === KEYCODE.C && event.altKey) {
event.preventDefault(); event.preventDefault();
clickStats(); clickStats();
} else if (event.key === KEY.E && event.altKey) { } else if (event.code === KEYCODE.E && event.altKey) {
event.preventDefault(); event.preventDefault();
clickScriptEditor(); clickScriptEditor();
} else if (event.key === KEY.S && event.altKey) { } else if (event.code === KEYCODE.S && event.altKey) {
event.preventDefault(); event.preventDefault();
clickActiveScripts(); clickActiveScripts();
} else if (event.key === KEY.H && event.altKey) { } else if (event.code === KEYCODE.H && event.altKey) {
event.preventDefault(); event.preventDefault();
clickHacknet(); clickHacknet();
} else if (event.key === KEY.W && event.altKey) { } else if (event.code === KEYCODE.W && event.altKey) {
event.preventDefault(); event.preventDefault();
clickCity(); clickCity();
} else if (event.key === KEY.J && event.altKey && !event.ctrlKey && !event.metaKey && canJob) { } else if (event.code === KEYCODE.J && event.altKey && !event.ctrlKey && !event.metaKey && canJob) {
// ctrl/cmd + alt + j is shortcut to open Chrome dev tools // ctrl/cmd + alt + j is shortcut to open Chrome dev tools
event.preventDefault(); event.preventDefault();
clickJob(); clickJob();
} else if (event.key === KEY.R && event.altKey) { } else if (event.code === KEYCODE.R && event.altKey) {
event.preventDefault(); event.preventDefault();
clickTravel(); clickTravel();
} else if (event.key === KEY.P && event.altKey) { } else if (event.code === KEYCODE.P && event.altKey) {
event.preventDefault(); event.preventDefault();
clickCreateProgram(); clickCreateProgram();
} else if (event.key === KEY.F && event.altKey) { } else if (event.code === KEYCODE.F && event.altKey) {
if (props.page == Page.Terminal && Settings.EnableBashHotkeys) { if (props.page == Page.Terminal && Settings.EnableBashHotkeys) {
return; return;
} }
event.preventDefault(); event.preventDefault();
clickFactions(); clickFactions();
} else if (event.key === KEY.A && event.altKey) { } else if (event.code === KEYCODE.A && event.altKey) {
event.preventDefault(); event.preventDefault();
clickAugmentations(); clickAugmentations();
} else if (event.key === KEY.U && event.altKey) { } else if (event.code === KEYCODE.U && event.altKey) {
event.preventDefault(); event.preventDefault();
clickTutorial(); clickTutorial();
} else if (event.key === KEY.B && event.altKey && props.player.bladeburner) { } else if (event.code === KEYCODE.B && event.altKey && props.player.bladeburner) {
event.preventDefault(); event.preventDefault();
clickBladeburner(); clickBladeburner();
} else if (event.key === KEY.G && event.altKey && props.player.gang) { } else if (event.code === KEYCODE.G && event.altKey && props.player.gang) {
event.preventDefault(); event.preventDefault();
clickGang(); clickGang();
} }
// if (event.key === KEY.O && event.altKey) {
// event.preventDefault();
// gameOptionsBoxOpen();
// }
} }
document.addEventListener("keydown", handleShortcuts); document.addEventListener("keydown", handleShortcuts);

@ -718,7 +718,11 @@ export class Terminal implements ITerminal {
} }
break; break;
case iTutorialSteps.TerminalCreateScript: case iTutorialSteps.TerminalCreateScript:
if (commandArray.length == 2 && commandArray[0] == "nano" && commandArray[1] == "n00dles.script") { if (
commandArray.length == 2 &&
commandArray[0] == "nano" &&
(commandArray[1] == "n00dles.script" || commandArray[1] == "n00dles.js")
) {
iTutorialNextStep(); iTutorialNextStep();
} else { } else {
this.error("Bad command. Please follow the tutorial"); this.error("Bad command. Please follow the tutorial");
@ -734,7 +738,11 @@ export class Terminal implements ITerminal {
} }
break; break;
case iTutorialSteps.TerminalRunScript: case iTutorialSteps.TerminalRunScript:
if (commandArray.length == 2 && commandArray[0] == "run" && commandArray[1] == "n00dles.script") { if (
commandArray.length == 2 &&
commandArray[0] == "run" &&
(commandArray[1] == "n00dles.script" || commandArray[1] == "n00dles.js")
) {
iTutorialNextStep(); iTutorialNextStep();
} else { } else {
this.error("Bad command. Please follow the tutorial"); this.error("Bad command. Please follow the tutorial");
@ -742,7 +750,11 @@ export class Terminal implements ITerminal {
} }
break; break;
case iTutorialSteps.ActiveScriptsToTerminal: case iTutorialSteps.ActiveScriptsToTerminal:
if (commandArray.length == 2 && commandArray[0] == "tail" && commandArray[1] == "n00dles.script") { if (
commandArray.length == 2 &&
commandArray[0] == "tail" &&
(commandArray[1] == "n00dles.script" || commandArray[1] == "n00dles.js")
) {
iTutorialNextStep(); iTutorialNextStep();
} else { } else {
this.error("Bad command. Please follow the tutorial"); this.error("Bad command. Please follow the tutorial");

@ -7,7 +7,7 @@ import Paper from "@mui/material/Paper";
import Popper from "@mui/material/Popper"; import Popper from "@mui/material/Popper";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { KEY } from "../../utils/helpers/keyCodes"; import { KEY, KEYCODE } from "../../utils/helpers/keyCodes";
import { ITerminal } from "../ITerminal"; import { ITerminal } from "../ITerminal";
import { IRouter } from "../../ui/Router"; import { IRouter } from "../../ui/Router";
import { IPlayer } from "../../PersonObjects/IPlayer"; import { IPlayer } from "../../PersonObjects/IPlayer";
@ -318,57 +318,57 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
// Extra Bash Emulation Hotkeys, must be enabled through options // Extra Bash Emulation Hotkeys, must be enabled through options
if (Settings.EnableBashHotkeys) { if (Settings.EnableBashHotkeys) {
if (event.key === KEY.A && event.ctrlKey) { if (event.code === KEYCODE.A && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("home"); moveTextCursor("home");
} }
if (event.key === KEY.E && event.ctrlKey) { if (event.code === KEYCODE.E && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("end"); moveTextCursor("end");
} }
if (event.key === KEY.B && event.ctrlKey) { if (event.code === KEYCODE.B && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("prevchar"); moveTextCursor("prevchar");
} }
if (event.key === KEY.B && event.altKey) { if (event.code === KEYCODE.B && event.altKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("prevword"); moveTextCursor("prevword");
} }
if (event.key === KEY.F && event.ctrlKey) { if (event.code === KEYCODE.F && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("nextchar"); moveTextCursor("nextchar");
} }
if (event.key === KEY.F && event.altKey) { if (event.code === KEYCODE.F && event.altKey) {
event.preventDefault(); event.preventDefault();
moveTextCursor("nextword"); moveTextCursor("nextword");
} }
if ((event.key === KEY.H || event.key === KEY.D) && event.ctrlKey) { if ((event.code === KEYCODE.H || event.code === KEYCODE.D) && event.ctrlKey) {
modifyInput("backspace"); modifyInput("backspace");
event.preventDefault(); event.preventDefault();
} }
if (event.key === KEY.W && event.ctrlKey) { if (event.code === KEYCODE.W && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
modifyInput("deletewordbefore"); modifyInput("deletewordbefore");
} }
if (event.key === KEY.D && event.altKey) { if (event.code === KEYCODE.D && event.altKey) {
event.preventDefault(); event.preventDefault();
modifyInput("deletewordafter"); modifyInput("deletewordafter");
} }
if (event.key === KEY.U && event.ctrlKey) { if (event.code === KEYCODE.U && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
modifyInput("clearbefore"); modifyInput("clearbefore");
} }
if (event.key === KEY.K && event.ctrlKey) { if (event.code === KEYCODE.K && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
modifyInput("clearafter"); modifyInput("clearafter");
} }

@ -134,20 +134,191 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
}, []); }, []);
function lineClass(s: string): string { function lineClass(s: string): string {
if (s === "error") { const lineClassMap: Record<string, string> = {
return classes.error; error: classes.error,
} success: classes.success,
if (s === "success") { info: classes.info,
return classes.success; warn: classes.warning,
} };
if (s === "info") { return lineClassMap[s] || classes.primary;
return classes.info; }
}
if (s === "warn") { function ansiCodeStyle(code: string | null): Record<string, any> {
return classes.warning; // The ANSI colors actually have the dark color set as default and require extra work to get
// bright colors. But these are rarely used or, if they are, are often re-mapped by the
// terminal emulator to brighter colors. So for foreground colors we use the bright color set
// and for background colors we use the dark color set. Of course, all colors are available
// via the longer ESC[n8;5;c] sequence (n={3,4}, c=color). Ideally, these 8-bit maps could
// be managed in the user preferences/theme.
const COLOR_MAP_BRIGHT: Record<string | number, string> = {
0: "#404040",
1: "#ff0000",
2: "#00ff00",
3: "#ffff00",
4: "#0000ff",
5: "#ff00ff",
6: "#00ffff",
7: "#ffffff",
};
const COLOR_MAP_DARK: Record<string | number, string> = {
0: "#000000",
1: "#800000",
2: "#008000",
3: "#808000",
4: "#000080",
5: "#800080",
6: "#008080",
7: "#c0c0c0",
};
const ansi2rgb = (code: number): string => {
/* eslint-disable yoda */
if (0 <= code && code < 8) {
// x8 RGB
return COLOR_MAP_BRIGHT[code];
}
if (8 <= code && code < 16) {
// x8 RGB - "High Intensity" (but here, actually the dark set)
return COLOR_MAP_DARK[code];
}
if (16 <= code && code < 232) {
// x216 RGB
const base = code - 16;
const ir = Math.floor(base / 36);
const ig = Math.floor((base % 36) / 6);
const ib = Math.floor((base % 6) / 1);
const r = ir <= 0 ? 0 : 55 + ir * 40;
const g = ig <= 0 ? 0 : 55 + ig * 40;
const b = ib <= 0 ? 0 : 55 + ib * 40;
return `rgb(${r}, ${g}, ${b})`;
}
if (232 <= code && code < 256) {
// x32 greyscale
const base = code - 232;
const grey = base * 10 + 8;
return `rgb(${grey}, ${grey}, ${grey})`;
}
// shouldn't get here (under normal circumstances), but just in case
return "initial";
};
type styleKey = "fontWeight" | "textDecoration" | "color" | "backgroundColor" | "display";
const style: {
fontWeight?: string;
textDecoration?: string;
color?: string;
backgroundColor?: string;
display?: string;
} = {};
if (code === null || code === "0") {
return style;
} }
return classes.primary; const codeParts = code
.split(";")
.map((p) => parseInt(p))
.filter(
(p, i, arr) =>
// If the sequence is 38;5 (x256 foreground color) or 48;5 (x256 background color),
// filter out the 5 so the next codePart after {38,48} is the color code.
p != 5 || i == 0 || (arr[i - 1] != 38 && arr[i - 1] != 48),
);
let nextStyleKey: styleKey | null = null;
codeParts.forEach((codePart) => {
/* eslint-disable yoda */
if (nextStyleKey !== null) {
style[nextStyleKey] = ansi2rgb(codePart);
nextStyleKey = null;
}
// Decorations
else if (codePart == 1) {
style.fontWeight = "bold";
} else if (codePart == 4) {
style.textDecoration = "underline";
}
// Forground Color (x8)
else if (30 <= codePart && codePart < 38) {
if (COLOR_MAP_BRIGHT[codePart % 10]) {
style.color = COLOR_MAP_BRIGHT[codePart % 10];
}
}
// Background Color (x8)
else if (40 <= codePart && codePart < 48) {
if (COLOR_MAP_DARK[codePart % 10]) {
style.backgroundColor = COLOR_MAP_DARK[codePart % 10];
}
}
// Forground Color (x256)
else if (codePart == 38) {
nextStyleKey = "color";
}
// Background Color (x256)
else if (codePart == 48) {
nextStyleKey = "backgroundColor";
}
});
// If a background color is set, render this as an inline block element (instead of inline)
// so that the background between lines (at least those that don't wrap) is uninterrupted.
if (style.backgroundColor !== undefined) {
style.display = "inline-block";
}
return style;
}
function parseOutputText(item: Output): React.ReactElement {
const parts = [];
// Some things, oddly, can cause item.text to not be a string (e.g. expr from the CLI), which
// causes .matchAll to throw. Ensure it's a string immediately
if (typeof item.text === "string") {
// eslint-disable-next-line no-control-regex
const ANSI_ESCAPE = new RegExp("\u{001b}\\[(?<code>.*?)m", "ug");
// Build a look-alike regex match to place at the front of the matches list
const INITIAL = {
0: "",
index: 0,
groups: { code: null },
};
const matches = [INITIAL, ...item.text.matchAll(ANSI_ESCAPE), null];
if (matches.length > 2) {
matches.slice(0, -1).forEach((m, i) => {
const n = matches[i + 1];
if (!m || m.index === undefined || m.groups === undefined) {
return;
}
const startIndex = m.index + m[0].length;
const stopIndex = n ? n.index : item.text.length;
const partText = item.text.slice(startIndex, stopIndex);
if (startIndex !== stopIndex) {
// Don't generate "empty" spans
parts.push({ code: m.groups.code, text: partText });
}
});
}
}
if (parts.length === 0) {
// For example, if the string was empty or there were no escape sequence matches
parts.push({ code: null, text: item.text });
}
return (
<Typography classes={{ root: lineClass(item.color) }} paragraph={false}>
{parts.map((part, index) => {
const spanStyle = ansiCodeStyle(part.code);
if (!_.isEmpty(spanStyle)) {
// Only wrap parts with spans if the calculated spanStyle is non-empty...
return (
<Typography key={index} paragraph={false} component="span" sx={spanStyle}>
{part.text}
</Typography>
);
} else {
// ... otherwise yield the unmodified, unwrapped part.
return part.text;
}
})}
</Typography>
);
} }
const classes = useStyles(); const classes = useStyles();
@ -160,7 +331,7 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
return ( return (
<ListItem key={i} classes={{ root: classes.nopadding }}> <ListItem key={i} classes={{ root: classes.nopadding }}>
<Typography classes={{ root: lineClass(item.color) }} paragraph={false}> <Typography classes={{ root: lineClass(item.color) }} paragraph={false}>
{item.text} {parseOutputText(item)}
</Typography> </Typography>
</ListItem> </ListItem>
); );

@ -1532,7 +1532,8 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
length += 2; length += 2;
} }
} }
return ans.length === length;
return ans.length <= length;
}, },
}, },
{ {
@ -1555,12 +1556,12 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
"You are given the following LZ-encoded string:\n", "You are given the following LZ-encoded string:\n",
`&nbsp; &nbsp; ${compressed}\n`, `&nbsp; &nbsp; ${compressed}\n`,
"Decode it and output the original string.\n\n", "Decode it and output the original string.\n\n",
"Example: decoding '5aaabc340533bca' chunk-by-chunk\n", "Example: decoding '5aaabb450723abb' chunk-by-chunk\n",
"&nbsp; &nbsp; 5aaabc &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabc\n", "&nbsp; &nbsp; 5aaabb &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabb\n",
"&nbsp; &nbsp; 5aaabc34 &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabcaab\n", "&nbsp; &nbsp; 5aaabb45 &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabbaaab\n",
"&nbsp; &nbsp; 5aaabc340 &nbsp; &nbsp; &nbsp; &nbsp;-> &nbsp;aaabcaab\n", "&nbsp; &nbsp; 5aaabb450 &nbsp; &nbsp; &nbsp; &nbsp;-> &nbsp;aaabbaaab\n",
"&nbsp; &nbsp; 5aaabc34053 &nbsp; &nbsp; &nbsp;-> &nbsp;aaabcaabaabaa\n", "&nbsp; &nbsp; 5aaabb45072 &nbsp; &nbsp; &nbsp;-> &nbsp;aaabbaaababababa\n",
"&nbsp; &nbsp; 5aaabc340533bca &nbsp;-> &nbsp;aaabcaabaabaabca", "&nbsp; &nbsp; 5aaabb450723abb &nbsp;-> &nbsp;aaabbaaababababaabb",
].join(" "); ].join(" ");
}, },
gen: (): string => { gen: (): string => {
@ -1591,21 +1592,21 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
`&nbsp; &nbsp; ${plaintext}\n`, `&nbsp; &nbsp; ${plaintext}\n`,
"Encode it using Lempel-Ziv encoding with the minimum possible output length.\n\n", "Encode it using Lempel-Ziv encoding with the minimum possible output length.\n\n",
"Examples (some have other possible encodings of minimal length):\n", "Examples (some have other possible encodings of minimal length):\n",
"&nbsp; &nbsp; abracadabra &nbsp; &nbsp;-> &nbsp;7abracad47\n", "&nbsp; &nbsp; abracadabra &nbsp; &nbsp; -> &nbsp;7abracad47\n",
"&nbsp; &nbsp; mississippi &nbsp; &nbsp;-> &nbsp;4miss433ppi\n", "&nbsp; &nbsp; mississippi &nbsp; &nbsp; -> &nbsp;4miss433ppi\n",
"&nbsp; &nbsp; aAAaAAaAaAA &nbsp; &nbsp;-> &nbsp;3aAA53035\n", "&nbsp; &nbsp; aAAaAAaAaAA &nbsp; &nbsp; -> &nbsp;3aAA53035\n",
"&nbsp; &nbsp; 2718281828 &nbsp; &nbsp; -> &nbsp;627182844\n", "&nbsp; &nbsp; 2718281828 &nbsp; &nbsp; &nbsp;-> &nbsp;627182844\n",
"&nbsp; &nbsp; abcdefghijk &nbsp; &nbsp;-> &nbsp;9abcdefghi02jk\n", "&nbsp; &nbsp; abcdefghijk &nbsp; &nbsp; -> &nbsp;9abcdefghi02jk\n",
"&nbsp; &nbsp; aaaaaaaaaaa &nbsp; &nbsp;-> &nbsp;1a911a\n", "&nbsp; &nbsp; aaaaaaaaaaaa &nbsp; &nbsp;-> &nbsp;3aaa91\n",
"&nbsp; &nbsp; aaaaaaaaaaaa &nbsp; -> &nbsp;1a912aa\n", "&nbsp; &nbsp; aaaaaaaaaaaaa &nbsp; -> &nbsp;1a91031\n",
"&nbsp; &nbsp; aaaaaaaaaaaaa &nbsp;-> &nbsp;1a91031", "&nbsp; &nbsp; aaaaaaaaaaaaaa &nbsp;-> &nbsp;1a91041",
].join(" "); ].join(" ");
}, },
gen: (): string => { gen: (): string => {
return comprLZGenerate(); return comprLZGenerate();
}, },
solver: (plain: string, ans: string): boolean => { solver: (plain: string, ans: string): boolean => {
return comprLZDecode(ans) === plain && ans.length === comprLZEncode(plain).length; return comprLZDecode(ans) === plain && ans.length <= comprLZEncode(plain).length;
}, },
}, },
]; ];

@ -4,7 +4,6 @@ import ReactDOM from "react-dom";
import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme"; import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme";
import { LoadingScreen } from "./ui/LoadingScreen"; import { LoadingScreen } from "./ui/LoadingScreen";
import { initElectron } from "./Electron"; import { initElectron } from "./Electron";
import { AlertEvents } from "./ui/React/AlertManager";
initElectron(); initElectron();
globalThis["React"] = React; globalThis["React"] = React;
globalThis["ReactDOM"] = ReactDOM; globalThis["ReactDOM"] = ReactDOM;

@ -9,6 +9,7 @@ import ArrowBackIos from "@mui/icons-material/ArrowBackIos";
import { ITutorialEvents } from "./ITutorialEvents"; import { ITutorialEvents } from "./ITutorialEvents";
import { CopyableText } from "../React/CopyableText"; import { CopyableText } from "../React/CopyableText";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import EqualizerIcon from "@mui/icons-material/Equalizer"; import EqualizerIcon from "@mui/icons-material/Equalizer";
import LastPageIcon from "@mui/icons-material/LastPage"; import LastPageIcon from "@mui/icons-material/LastPage";
@ -27,6 +28,7 @@ import {
iTutorialSteps, iTutorialSteps,
iTutorialEnd, iTutorialEnd,
} from "../../InteractiveTutorial"; } from "../../InteractiveTutorial";
import { NSSelection } from "./NSSelection";
interface IContent { interface IContent {
content: React.ReactElement; content: React.ReactElement;
@ -45,9 +47,23 @@ const useStyles = makeStyles((theme: Theme) =>
}), }),
); );
enum Language {
None,
NS1,
NS2,
}
export function InteractiveTutorialRoot(): React.ReactElement { export function InteractiveTutorialRoot(): React.ReactElement {
const [nsSelectionOpen, setNSSelectionOpen] = useState(false);
const [language, setLanguage] = useState(Language.None);
const classes = useStyles(); const classes = useStyles();
const tutorialScriptName = {
[Language.None]: "n00dles.script",
[Language.NS1]: "n00dles.script",
[Language.NS2]: "n00dles.js",
}[language];
const contents: { [number: string]: IContent | undefined } = { const contents: { [number: string]: IContent | undefined } = {
[iTutorialSteps.Start as number]: { [iTutorialSteps.Start as number]: {
content: ( content: (
@ -66,6 +82,47 @@ export function InteractiveTutorialRoot(): React.ReactElement {
), ),
canNext: true, canNext: true,
}, },
[iTutorialSteps.NSSelection as number]: {
content: (
<>
<Typography>The tutorial will adjust to your programming ability.</Typography>
<Typography>Bitburner has 2 types of scripts:</Typography>
<List>
<ListItem>
<Typography>NS1: Javascript from 2009, very simple. Recommended for beginners to programming.</Typography>
</ListItem>
<ListItem>
<Typography>
NS2: Native, modern Javascript. Recommended if you know any programming language or are serious about
learning programming.
</Typography>
</ListItem>
</List>
<Typography>
Both are available at all time and interchangeably. This choice is only for the tutorial.
</Typography>
<Button
onClick={() => {
setLanguage(Language.NS1);
iTutorialNextStep();
}}
>
Use NS1
</Button>
<Button
onClick={() => {
setLanguage(Language.NS2);
iTutorialNextStep();
}}
>
Use NS2
</Button>
<Button onClick={() => setNSSelectionOpen(true)}>More info</Button>
<br />
</>
),
canNext: false,
},
[iTutorialSteps.GoToCharacterPage as number]: { [iTutorialSteps.GoToCharacterPage as number]: {
content: ( content: (
<> <>
@ -321,7 +378,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
<Typography classes={{ root: classes.textfield }}>{"[home ~/]> nano"}</Typography> <Typography classes={{ root: classes.textfield }}>{"[home ~/]> nano"}</Typography>
<Typography>Scripts must end with the .script extension. Let's make a script now by entering </Typography> <Typography>Scripts must end with the .script extension. Let's make a script now by entering </Typography>
<Typography classes={{ root: classes.textfield }}>{"[home ~/]> nano n00dles.script"}</Typography> <Typography classes={{ root: classes.textfield }}>{`[home ~/]> nano ${tutorialScriptName}`}</Typography>
<Typography> <Typography>
after the hack command finishes running (Sidenote: Pressing ctrl + c will end a command like hack early) after the hack command finishes running (Sidenote: Pressing ctrl + c will end a command like hack early)
@ -334,16 +391,28 @@ export function InteractiveTutorialRoot(): React.ReactElement {
content: ( content: (
<> <>
<Typography> <Typography>
This is the script editor. You can use it to program your scripts. Scripts are written in a simplified This is the script editor. You can use it to program your scripts.{" "}
version of javascript. Copy and paste the following code into the script editor: <br /> {language !== Language.NS2 && <>Scripts are written in a simplified version of javascript.</>} Copy and
paste the following code into the script editor: <br />
</Typography> </Typography>
<Typography classes={{ root: classes.code }}> <Typography classes={{ root: classes.code }}>
<CopyableText {language !== Language.NS2 && (
value={`while(true) { <CopyableText
value={`while(true) {
hack('n00dles'); hack('n00dles');
}`} }`}
/> />
)}
{language === Language.NS2 && (
<CopyableText
value={`export async function main(ns) {
while(true) {
await ns.hack('n00dles');
}
}`}
/>
)}
</Typography> </Typography>
<Typography> <Typography>
For anyone with basic programming experience, this code should be straightforward. This script will For anyone with basic programming experience, this code should be straightforward. This script will
@ -378,7 +447,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
<Typography> <Typography>
We have 8GB of free RAM on this machine, which is enough to run our script. Let's run our script using We have 8GB of free RAM on this machine, which is enough to run our script. Let's run our script using
</Typography> </Typography>
<Typography classes={{ root: classes.textfield }}>{"[home ~/]> run n00dles.script"}</Typography> <Typography classes={{ root: classes.textfield }}>{`[home ~/]> run ${tutorialScriptName}`}</Typography>
</> </>
), ),
canNext: false, canNext: false,
@ -425,7 +494,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
One last thing about scripts, each active script contains logs that detail what it's doing. We can check One last thing about scripts, each active script contains logs that detail what it's doing. We can check
these logs using the tail command. Do that now for the script we just ran by typing{" "} these logs using the tail command. Do that now for the script we just ran by typing{" "}
</Typography> </Typography>
<Typography classes={{ root: classes.textfield }}>{"[home ~/]> tail n00dles.script"}</Typography> <Typography classes={{ root: classes.textfield }}>{`[home ~/]> tail ${tutorialScriptName}`}</Typography>
</> </>
), ),
canNext: false, canNext: false,
@ -447,14 +516,6 @@ export function InteractiveTutorialRoot(): React.ReactElement {
in the main navigation menu to look at the documentation. in the main navigation menu to look at the documentation.
<br /> <br />
<br /> <br />
If you know even a little bit of programming it is highly recommended you use NS2 instead. You will enjoy
the game much more. NS1 files end with .script and are a subset of javascript. NS2 files end with .js and
are full speed native javascript.
<br />
<br />
You can learn more about the difference between them later in the documentation.
<br />
<br />
For now, let's move on to something else! For now, let's move on to something else!
</Typography> </Typography>
</> </>
@ -549,25 +610,30 @@ export function InteractiveTutorialRoot(): React.ReactElement {
const content = contents[step]; const content = contents[step];
if (content === undefined) throw new Error("error in the tutorial"); if (content === undefined) throw new Error("error in the tutorial");
return ( return (
<Paper square sx={{ maxWidth: "70vw", p: 2 }}> <>
{content.content} <NSSelection open={nsSelectionOpen} onClose={() => setNSSelectionOpen(false)} />
{step !== iTutorialSteps.TutorialPageInfo && ( <Paper square sx={{ maxWidth: "70vw", p: 2 }}>
<> {content.content}
<IconButton onClick={iTutorialPrevStep} aria-label="previous"> {step !== iTutorialSteps.TutorialPageInfo && (
<ArrowBackIos /> <>
</IconButton> {step !== iTutorialSteps.Start && (
{content.canNext && ( <IconButton onClick={iTutorialPrevStep} aria-label="previous">
<IconButton onClick={iTutorialNextStep} aria-label="next"> <ArrowBackIos />
<ArrowForwardIos /> </IconButton>
</IconButton> )}
)} {content.canNext && (
</> <IconButton onClick={iTutorialNextStep} aria-label="next">
)} <ArrowForwardIos />
<br /> </IconButton>
<br /> )}
<Button onClick={iTutorialEnd}> </>
{step !== iTutorialSteps.TutorialPageInfo ? "SKIP TUTORIAL" : "FINISH TUTORIAL"} )}
</Button> <br />
</Paper> <br />
<Button onClick={iTutorialEnd}>
{step !== iTutorialSteps.TutorialPageInfo ? "SKIP TUTORIAL" : "FINISH TUTORIAL"}
</Button>
</Paper>
</>
); );
} }

@ -0,0 +1,82 @@
import Editor from "@monaco-editor/react";
import { Tab, Tabs, Typography } from "@mui/material";
import React from "react";
import { Modal } from "../React/Modal";
import * as monaco from "monaco-editor";
type IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor;
interface IProps {
open: boolean;
onClose: () => void;
}
const ns1Example = `while(true) {
hack('n00dles');
}`;
const ns2Example = `/** @param {NS} ns */
export async function main(ns) {
while(true) {
await ns.hack('n00dles');
}
}`;
export function NSSelection(props: IProps): React.ReactElement {
const [value, setValue] = React.useState(0);
function handleChange(event: React.SyntheticEvent, tab: number): void {
setValue(tab);
}
function onMount(editor: IStandaloneCodeEditor): void {
editor.updateOptions({ readOnly: true });
}
return (
<Modal open={props.open} onClose={props.onClose}>
<Tabs variant="fullWidth" value={value} onChange={handleChange}>
<Tab label="NS1" />
<Tab label="NS2" />
</Tabs>
{value === 0 && (
<>
<Typography>
These scripts end with '.script'. Using a very old interpreted version of javascript. It is perfect for
beginner to programming.
</Typography>
<Typography>Example script using NS1:</Typography>
<Editor
loading={<></>}
defaultLanguage="javascript"
defaultValue={ns1Example}
height={`${300}px`}
theme={"vs-dark"}
onMount={onMount}
options={{ fontSize: 30 }}
/>
</>
)}
{value === 1 && (
<>
<Typography>
These scripts end with '.js'. Scripts using ns2 are running natively along the game. If you know any
programming language you should be using this. However if you're unfamiliar with javascript Promises you
might want to read up on them a little bit before diving in.
</Typography>
<Typography>Example script using NS2:</Typography>
<Editor
loading={<></>}
defaultLanguage="javascript"
defaultValue={ns2Example}
height={`${300}px`}
theme={"vs-dark"}
options={{ fontSize: 30 }}
/>
</>
)}
</Modal>
);
}

@ -12,6 +12,7 @@ const useStyles = makeStyles((theme: Theme) =>
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
zIndex: 999999,
}, },
paper: { paper: {
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,

@ -105,6 +105,13 @@ export function comprLZEncode(plain: string): string {
// start new literal // start new literal
set(new_state, 0, 1, string + length + offset); set(new_state, 0, 1, string + length + offset);
// end current backreference and start new backreference
for (let new_offset = 1; new_offset <= Math.min(9, i); ++new_offset) {
if (plain[i - new_offset] === c) {
set(new_state, new_offset, 1, string + length + offset + "0");
}
}
} }
} }

@ -20,6 +20,6 @@ export function exceptionAlert(e: IError | string): void {
"This is a bug, please report to game developer with this " + "This is a bug, please report to game developer with this " +
"message as well as details about how to reproduce the bug.<br><br>" + "message as well as details about how to reproduce the bug.<br><br>" +
"If you want to be safe, I suggest refreshing the game WITHOUT saving so that your " + "If you want to be safe, I suggest refreshing the game WITHOUT saving so that your " +
"safe doesn't get corrupted", "save doesn't get corrupted",
); );
} }

@ -1,5 +1,5 @@
/** /**
* Keyboard key codes * Keyboard key codes as returned by event.key
*/ */
export enum KEY { export enum KEY {
//SHIFT: 16, // Check by `&& event.shiftKey` //SHIFT: 16, // Check by `&& event.shiftKey`
@ -70,3 +70,69 @@ export enum KEY {
Y = "y", Y = "y",
Z = "z", Z = "z",
} }
/**
* Keyboard key codes as returned by event.code
*/
export enum KEYCODE {
//SHIFT: 16, // Check by `&& event.shiftKey`
//CTRL: 17, // Check by `&& event.ctrlKey`
//ALT: 18, // Check by `&& event.altKey`
ENTER = "Enter",
ESC = "Escape",
TAB = "Tab",
SPACE = "Space",
BACKSPACE = "Backspace",
UP_ARROW = "ArrowUp",
DOWN_ARROW = "ArrowDown",
LEFT_ARROW = "ArrowLeft",
RIGHT_ARROW = "ArrowRight",
BACKWARD_SLASH = "Backslash",
BACKQUOTE = "Backquote",
COMMA = "Comma",
DOT = "Period",
EQUAL = "Equal",
FORWARD_SLASH = "Slash",
HYPHEN = "Minus",
SEMICOLON = "Semicolon",
QUOTE = "Quote",
k0 = "Digit0",
k1 = "Digit1",
k2 = "Digit2",
k3 = "Digit3",
k4 = "Digit4",
k5 = "Digit5",
k6 = "Digit6",
k7 = "Digit7",
k8 = "Digit8",
k9 = "Digit9",
A = "KeyA",
B = "KeyB",
C = "KeyC",
D = "KeyD",
E = "KeyE",
F = "KeyF",
G = "KeyG",
H = "KeyH",
I = "KeyI",
J = "KeyJ",
K = "KeyK",
L = "KeyL",
M = "KeyM",
N = "KeyN",
O = "KeyO",
P = "KeyP",
Q = "KeyQ",
R = "KeyR",
S = "KeyS",
T = "KeyT",
U = "KeyU",
V = "KeyV",
W = "KeyW",
X = "KeyX",
Y = "KeyY",
Z = "KeyZ",
}