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. |
| | | |
| | | Example: decoding '5aaabc340533bca' chunk-by-chunk |
| | | 5aaabc -> aaabc |
| | | 5aaabc34 -> aaabcaab |
| | | 5aaabc340 -> aaabcaab |
| | | 5aaabc34053 -> aaabcaabaabaa |
| | | 5aaabc340533bca -> aaabcaabaabaabca |
| | | Example: decoding '5aaabb450723abb' chunk-by-chunk |
| | | 5aaabb -> aaabb |
| | | 5aaabb45 -> aaabbaaab |
| | | 5aaabb450 -> aaabbaaab |
| | | 5aaabb45072 -> aaabbaaababababa |
| | | 5aaabb450723abb -> aaabbaaababababaabb |
+-----------------------------------------+------------------------------------------------------------------------------------------+
| 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 |
@ -361,12 +361,12 @@ The list contains the name of (i.e. the value returned by
| | | possible output length. |
| | | |
| | | Examples (some have other possible encodings of minimal length): |
| | | abracadabra -> 7abracad47 |
| | | mississippi -> 4miss433ppi |
| | | aAAaAAaAaAA -> 3aAA53035 |
| | | 2718281828 -> 627182844 |
| | | abcdefghijk -> 9abcdefghi02jk |
| | | aaaaaaaaaaa -> 1a911a |
| | | aaaaaaaaaaaa -> 1a912aa |
| | | aaaaaaaaaaaaa -> 1a91031 |
| | | abracadabra -> 7abracad47 |
| | | mississippi -> 4miss433ppi |
| | | aAAaAAaAaAA -> 3aAA53035 |
| | | 2718281828 -> 627182844 |
| | | abcdefghijk -> 9abcdefghi02jk |
| | | aaaaaaaaaaaa -> 3aaa91 |
| | | aaaaaaaaaaaaa -> 1a91031 |
| | | aaaaaaaaaaaaaa -> 1a91041 |
+-----------------------------------------+------------------------------------------------------------------------------------------+

@ -334,7 +334,7 @@
},
"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."
},
"4S": {

@ -9,6 +9,18 @@ import { Money } from "../ui/React/Money";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver";
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 {
info: string | JSX.Element;
@ -410,10 +422,10 @@ function generateStatsDescription(mults: IMap<number>, programs?: string[], star
}
export class Augmentation {
// How much money this costs to buy
// How much money this costs to buy before multipliers
baseCost = 0;
// How much faction reputation is required to unlock this
// How much faction reputation is required to unlock this before multipliers
baseRepRequirement = 0;
// 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)
isSpecial = false;
// Augmentation level - for repeatable Augs like NeuroFlux Governor
level = 0;
// Name of Augmentation
name = "";
@ -438,12 +447,6 @@ export class Augmentation {
// The Player/Person classes
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: string[] = [];
@ -461,17 +464,15 @@ export class Augmentation {
this.prereqs = params.prereqs ? params.prereqs : [];
this.baseRepRequirement = params.repCost;
Object.freeze(this.baseRepRequirement);
this.baseCost = params.moneyCost;
this.startingCost = this.baseCost;
this.startingRepRequirement = this.baseRepRequirement;
Object.freeze(this.baseCost);
this.factions = params.factions;
if (params.isSpecial) {
this.isSpecial = true;
}
this.level = 0;
// Set multipliers
if (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
addToAllFactions(): void {
for (const fac of Object.keys(Factions)) {

@ -1,5 +1,5 @@
import { Augmentation } from "./Augmentation";
import { Augmentations } from "./Augmentations";
import { StaticAugmentations } from "./StaticAugmentations";
import { PlayerOwnedAugmentation, IPlayerOwnedAugmentation } from "./PlayerOwnedAugmentation";
import { AugmentationNames } from "./data/AugmentationNames";
@ -20,30 +20,11 @@ import {
initNeuroFluxGovernor,
initUnstableCircadianModulator,
} from "./data/AugmentationCreator";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { Router } from "../ui/GameRoot";
export function AddToAugmentations(aug: Augmentation): void {
export function AddToStaticAugmentations(aug: Augmentation): void {
const name = aug.name;
Augmentations[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;
StaticAugmentations[name] = aug;
}
function createAugmentations(): void {
@ -67,96 +48,37 @@ function resetFactionAugmentations(): void {
function initAugmentations(): void {
resetFactionAugmentations();
clearObject(Augmentations);
clearObject(StaticAugmentations);
createAugmentations();
updateAugmentationCosts();
Player.reapplyAllAugmentations();
}
function getBaseAugmentationPriceMultiplier(): number {
export function getBaseAugmentationPriceMultiplier(): number {
return CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][Player.sourceFileLvl(11)];
}
export function getGenericAugmentationPriceMultiplier(): number {
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)
function resetAugmentation(aug: Augmentation): void {
aug.addToFactions(aug.factions);
const name = aug.name;
if (augmentationExists(name)) {
delete Augmentations[name];
delete StaticAugmentations[name];
}
AddToAugmentations(aug);
AddToStaticAugmentations(aug);
}
function applyAugmentation(aug: IPlayerOwnedAugmentation, reapply = false): void {
const augObj = Augmentations[aug.name];
const staticAugmentation = StaticAugmentations[aug.name];
// Apply multipliers
for (const mult of Object.keys(augObj.mults)) {
const v = Player.getMult(mult) * augObj.mults[mult];
for (const mult of Object.keys(staticAugmentation.mults)) {
const v = Player.getMult(mult) * staticAugmentation.mults[mult];
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
if (aug.name === AugmentationNames.CongruityImplant && !reapply) {
Player.entropy = 0;
@ -185,7 +107,7 @@ function installAugmentations(force?: boolean): boolean {
}
for (let i = 0; i < Player.queuedAugmentations.length; ++i) {
const ownedAug = Player.queuedAugmentations[i];
const aug = Augmentations[ownedAug.name];
const aug = StaticAugmentations[ownedAug.name];
if (aug == null) {
console.error(`Invalid augmentation: ${ownedAug.name}`);
continue;
@ -215,7 +137,7 @@ function installAugmentations(force?: boolean): boolean {
}
function augmentationExists(name: string): boolean {
return Augmentations.hasOwnProperty(name);
return StaticAugmentations.hasOwnProperty(name);
}
export function isRepeatableAug(aug: Augmentation): boolean {

@ -1,4 +1,4 @@
import { Augmentation } from "./Augmentation";
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",
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
MightOfAres = "SoA - Might of Ares", // slash
WisdomOfAthena = "SoA - Wisdom of Athena", // bracket
@ -135,10 +124,4 @@ export enum AugmentationNames {
HuntOfArtemis = "SoA - Hunt of Artemis", // mine
KnowledgeOfApollo = "SoA - Knowledge of Apollo", // wire
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 { IPlayer } from "../../PersonObjects/IPlayer";
import { AugmentationNames } from "../data/AugmentationNames";
import { Augmentations } from "../Augmentations";
import { StaticAugmentations } from "../StaticAugmentations";
import { CONSTANTS } from "../../Constants";
import { formatNumber } from "../../utils/StringHelperFunctions";
import { Info } from "@mui/icons-material";
@ -39,7 +39,9 @@ const NeuroFluxDisplay = ({ player }: NFGDisplayProps): React.ReactElement => {
<Typography variant="h5" color={Settings.theme.info}>
NeuroFlux Governor - Level {level}
</Typography>
<Typography color={Settings.theme.info}>{Augmentations[AugmentationNames.NeuroFluxGovernor].stats}</Typography>
<Typography color={Settings.theme.info}>
{StaticAugmentations[AugmentationNames.NeuroFluxGovernor].stats}
</Typography>
</Paper>
) : (
<></>

@ -13,7 +13,7 @@ import React, { useState } from "react";
import { OwnedAugmentationsOrderSetting } from "../../Settings/SettingEnums";
import { Settings } from "../../Settings/Settings";
import { use } from "../../ui/Context";
import { Augmentations } from "../Augmentations";
import { StaticAugmentations } from "../StaticAugmentations";
import { AugmentationNames } from "../data/AugmentationNames";
export function InstalledAugmentations(): React.ReactElement {
@ -77,7 +77,7 @@ export function InstalledAugmentations(): React.ReactElement {
</Typography>
<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 tooltip = (

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

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

@ -44,7 +44,7 @@ export function PurchaseAugmentationModal(props: IProps): React.ReactElement {
<br />
<br />
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 />
</Typography>

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

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

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

@ -106,6 +106,7 @@ interface IProps {
onClose: () => void;
mat: Material;
warehouse: Warehouse;
disablePurchaseLimit: boolean;
}
// Create a popup that lets the player purchase a Material
@ -143,6 +144,7 @@ export function PurchaseMaterialModal(props: IProps): React.ReactElement {
<Typography>
Enter the amount of {props.mat.name} you would like to purchase per second. This material's cost changes
constantly.
{props.disablePurchaseLimit ? "Note: Purchase amount is disabled as smart supply is enabled" : ""}
</Typography>
<TextField
value={buyAmt}
@ -150,10 +152,15 @@ export function PurchaseMaterialModal(props: IProps): React.ReactElement {
autoFocus={true}
placeholder="Purchase amount"
type="number"
disabled={props.disablePurchaseLimit}
onKeyDown={onKeyDown}
/>
<Button onClick={purchaseMaterial}>Confirm</Button>
<Button onClick={clearPurchase}>Clear Purchase</Button>
<Button disabled={props.disablePurchaseLimit} onClick={purchaseMaterial}>
Confirm
</Button>
<Button disabled={props.disablePurchaseLimit} onClick={clearPurchase}>
Clear Purchase
</Button>
{division.hasResearch("Bulk Purchasing") && (
<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 { PlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
@ -18,7 +18,6 @@ import {
import { dialogBoxCreate } from "../ui/React/DialogBox";
import { InvitationEvent } from "./ui/InvitationModal";
import { FactionNames } from "./data/FactionNames";
import { updateAugmentationCosts, getNextNeuroFluxLevel } from "../Augmentation/AugmentationHelpers";
import { SFC32RNG } from "../Casino/RNG";
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 {
const hasPrereqs = hasAugmentationPrereqs(aug);
const augCosts = aug.getCost(Player);
if (!hasPrereqs) {
const txt = `You must first purchase or install ${aug.prereqs
.filter((req) => !Player.hasAugmentation(req))
@ -68,28 +68,26 @@ export function purchaseAugmentation(aug: Augmentation, fac: Faction, sing = fal
} else {
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;
if (sing) {
return 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;
if (sing) {
return 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);
if (aug.name == AugmentationNames.NeuroFluxGovernor) {
queuedAugmentation.level = getNextNeuroFluxLevel();
queuedAugmentation.level = aug.getLevel(Player);
}
Player.queuedAugmentations.push(queuedAugmentation);
Player.loseMoney(aug.baseCost, "augmentations");
updateAugmentationCosts();
Player.loseMoney(augCosts.moneyCost, "augmentations");
if (sing) {
return "You purchased " + aug.name;
@ -141,14 +139,14 @@ export function processPassiveFactionRepGain(numCycles: number): void {
export const getFactionAugmentationsFiltered = (player: IPlayer, faction: Faction): string[] => {
// If player has a gang with this faction, return (almost) all augmentations
if (player.hasGangWith(faction.name)) {
let augs = Object.values(Augmentations);
let augs = Object.values(StaticAugmentations);
// 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) {
// 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)}`);

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

@ -16,12 +16,10 @@ import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";
import { Faction } from "../Faction";
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 { FactionNames } from "../data/FactionNames";
import { GangConstants } from "../../Gang/data/Constants";
import { GangButton } from "./GangButton";
type IProps = {
@ -30,7 +28,6 @@ type IProps = {
};
// 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 =
"Complete hacking contracts for your faction. " +
"Your effectiveness, which determines how much " +

@ -189,9 +189,9 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
title={
<Typography>
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
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.
</Typography>
}
@ -210,7 +210,7 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
title={
<Typography>
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>
}
>

@ -60,15 +60,22 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
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) -
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);
upgradeLevelButton = (
<Tooltip
title={
<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>
}
>
@ -109,20 +116,36 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
multiplier = Math.min(levelsToMax, purchaseMult as number);
}
const increase =
const base_increase =
calculateHashGainRate(
node.level,
0,
node.maxRam * Math.pow(2, multiplier),
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 =
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);
upgradeRamButton = (
<Tooltip
title={
<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>
}
>
@ -155,15 +178,22 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
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) -
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);
upgradeCoresButton = (
<Tooltip
title={
<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>
}
>
@ -232,9 +262,31 @@ export function HacknetServerElem(props: IProps): React.ReactElement {
<Typography>Production:</Typography>
</TableCell>
<TableCell colSpan={2}>
<Typography>
<Hashes hashes={node.totalHashesGenerated} /> (<HashRate hashes={node.hashRate} />)
</Typography>
<Tooltip
title={
<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>
</TableRow>
<TableRow>

@ -1,15 +1,14 @@
import { Paper, Typography } from "@mui/material";
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 { 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 {
[key: string]: number;
@ -48,24 +47,18 @@ export function BackwardGame(props: IMinigameProps): React.ReactElement {
}
return (
<Grid container spacing={3}>
<>
<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>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid>
<Grid item xs={6}>
<Typography style={{ transform: hasAugment ? "none" : "scaleX(-1)", marginLeft: hasAugment ? "50%" : "none" }}>
{answer}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography style={{ transform: hasAugment ? "none" : "scaleX(-1)" }}>{answer}</Typography>
<Typography>
{guess}
<BlinkingCursor />
</Typography>
</Grid>
</Grid>
</Paper>
</>
);
}

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

@ -1,15 +1,14 @@
import { Paper, Typography } from "@mui/material";
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 { 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 {
[key: string]: number;
@ -84,16 +83,16 @@ export function BracketGame(props: IMinigameProps): React.ReactElement {
}
return (
<Grid container spacing={3}>
<>
<GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}>
<Paper sx={{ display: "grid", justifyItems: "center" }}>
<Typography variant="h4">Close the brackets</Typography>
<Typography style={{ fontSize: "5em" }}>
{`${left}${right}`}
<BlinkingCursor />
</Typography>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid>
</Grid>
</Paper>
</>
);
}

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

@ -1,21 +1,20 @@
import { Paper, Typography } from "@mui/material";
import React, { useState } from "react";
import Grid from "@mui/material/Grid";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
import { GameTimer } from "./GameTimer";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player";
import {
random,
downArrowSymbol,
getArrow,
getInverseArrow,
leftArrowSymbol,
random,
rightArrowSymbol,
upArrowSymbol,
downArrowSymbol,
} from "../utils";
import { interpolate } from "./Difficulty";
import Typography from "@mui/material/Typography";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player";
import { GameTimer } from "./GameTimer";
import { IMinigameProps } from "./IMinigameProps";
import { KeyHandler } from "./KeyHandler";
interface Difficulty {
[key: string]: number;
@ -55,14 +54,14 @@ export function CheatCodeGame(props: IMinigameProps): React.ReactElement {
}
return (
<Grid container spacing={3}>
<>
<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">{code[index]}</Typography>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid>
</Grid>
</Paper>
</>
);
}

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

@ -1,15 +1,14 @@
import { Paper, Typography, Box } from "@mui/material";
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 { 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 {
[key: string]: number;
@ -19,6 +18,12 @@ interface Difficulty {
symbols: number;
}
interface GridItem {
content: string;
color: string;
selected?: boolean;
}
const difficulties: {
Trivial: 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";
return (
<Grid container spacing={3}>
<>
<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="h5" color={Settings.theme.primary}>
Targets:{" "}
{answers.map((a, i) => {
if (i == currentAnswerIndex)
return (
<span key={`${i}`} style={{ fontSize: "1em", color: "blue" }}>
<span key={`${i}`} style={{ fontSize: "1em", color: Settings.theme.infolight }}>
{a}&nbsp;
</span>
);
@ -99,34 +119,30 @@ export function Cyberpunk2077Game(props: IMinigameProps): React.ReactElement {
})}
</Typography>
<br />
{grid.map((line, y) => (
<div key={y}>
<Typography>
{line.map((cell, x) => {
const isCorrectAnswer = cell === answers[currentAnswerIndex];
if (x == pos[0] && y == pos[1]) {
return (
<span key={`${x}${y}`} style={{ fontSize: fontSize, color: "blue" }}>
{cell}&nbsp;
</span>
);
}
const optionColor = hasAugment && !isCorrectAnswer ? Settings.theme.disabled : Settings.theme.primary;
return (
<span key={`${x}${y}`} style={{ fontSize: fontSize, color: optionColor }}>
{cell}&nbsp;
</span>
);
})}
<Box
sx={{
display: "grid",
gridTemplateColumns: `repeat(${Math.round(difficulty.width)}, 1fr)`,
gap: 1,
}}
>
{flatGrid.map((item) => (
<Typography
sx={{
fontSize: fontSize,
color: item.color,
border: item.selected ? `2px solid ${Settings.theme.infolight}` : "unset",
lineHeight: "unset",
p: item.selected ? "2px" : "4px",
}}
>
{item.content}
</Typography>
<br />
</div>
))}
))}
</Box>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid>
</Grid>
</Paper>
</>
);
}

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

@ -1,4 +1,4 @@
import Grid from "@mui/material/Grid";
import { Paper } from "@mui/material";
import React, { useEffect, useState } from "react";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { use } from "../../ui/Context";
@ -7,6 +7,7 @@ import { ProgressBar } from "../../ui/React/Progress";
interface IProps {
millis: number;
onExpire: () => void;
noPaper?: boolean;
}
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
// TODO(hydroflame): there's like a bug where it triggers the end before the
// bar physically reaches the end
return (
<Grid item xs={12}>
return props.noPaper ? (
<ProgressBar variant="determinate" value={v} color="primary" />
) : (
<Paper sx={{ p: 1, mb: 1 }}>
<ProgressBar variant="determinate" value={v} color="primary" />
</Grid>
</Paper>
);
}

@ -1,9 +1,9 @@
import React, { useState } from "react";
import { Intro } from "./Intro";
import { Game } from "./Game";
import { Location } from "../../Locations/Location";
import { use } from "../../ui/Context";
import { calculateDifficulty, calculateReward } from "../formulas/game";
import { Game } from "./Game";
import { Intro } from "./Intro";
interface IProps {
location: Location;
}
@ -22,24 +22,24 @@ export function InfiltrationRoot(props: IProps): React.ReactElement {
router.toCity();
}
if (!start) {
return (
<Intro
Location={props.location}
Difficulty={difficulty}
MaxLevel={props.location.infiltrationData.maxClearanceLevel}
start={() => setStart(true)}
cancel={cancel}
/>
);
}
return (
<Game
StartingDifficulty={startingSecurityLevel}
Difficulty={difficulty}
Reward={reward}
MaxLevel={props.location.infiltrationData.maxClearanceLevel}
/>
<div style={{ display: "flex", alignItems: "center", height: "calc(100vh - 16px)" }}>
{start ? (
<Game
StartingDifficulty={startingSecurityLevel}
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 { Location } from "../../Locations/Location";
import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { Settings } from "../../Settings/Settings";
import { numeralWrapper } from "../../ui/numeralFormat";
interface IProps {
@ -41,9 +41,9 @@ function coloredArrow(difficulty: number): JSX.Element {
} else {
return (
<>
{arrowPart("white", difficulty * 13)}
{arrowPart("orange", (difficulty - 1) * 13)}
{arrowPart("red", (difficulty - 2) * 13)}
{arrowPart(Settings.theme.primary, difficulty * 13)}
{arrowPart(Settings.theme.warning, (difficulty - 1) * 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 {
return (
<>
<Grid container spacing={3}>
<Grid item xs={10}>
<Typography variant="h4">Infiltrating {props.Location.name}</Typography>
</Grid>
<Grid item xs={10}>
<Typography variant="h5" color="primary">
Maximum level: {props.MaxLevel}
</Typography>
</Grid>
<Grid item xs={10}>
<Typography variant="h5" color="primary">
Difficulty: {numeralWrapper.format(props.Difficulty * 33.3333, "0")} / 100
</Typography>
</Grid>
<Container sx={{ alignItems: "center" }}>
<Paper sx={{ p: 1, mb: 1, display: "grid", justifyItems: "center" }}>
<Typography variant="h4">
Infiltrating <b>{props.Location.name}</b>
</Typography>
<Typography variant="h6">
<b>Maximum Level: </b>
{props.MaxLevel}
</Typography>
<Typography
variant="h6"
sx={{
color:
props.Difficulty > 2
? 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 && (
<Grid item xs={10}>
<Typography variant="h5" color="primary">
Warning: This location is too heavily guarded for your current stats, try training or finding an easier
location.
</Typography>
</Grid>
)}
<Typography sx={{ lineHeight: "1em", whiteSpace: "pre" }}>[{coloredArrow(props.Difficulty)}]</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{`▲ ▲ ▲ ▲`}</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{` Trivial Normal Hard Impossible`}</Typography>
</Paper>
<Grid item xs={10}>
<Typography sx={{ lineHeight: "1em", whiteSpace: "pre" }}>[{coloredArrow(props.Difficulty)}]</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{` ^ ^ ^ ^`}</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{` Trivial Normal Hard Impossible`}</Typography>
</Grid>
<Grid item xs={10}>
<Paper sx={{ p: 1, display: "grid", justifyItems: "center" }}>
<Typography sx={{ width: "75%", textAlign: "center" }}>
<b>Infiltration</b> is a series of short minigames that get progressively harder. You take damage for failing
them. Reaching the maximum level rewards you with intel that you can trade for money or reputation.
<br />
<br />
<b>Gameplay:</b>
</Typography>
<ul>
<Typography>
Infiltration is a series of short minigames that get progressively harder. You take damage for failing them.
Reaching the maximum level rewards you with intel you can trade for money or reputation.
<li>
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>
<br />
<Typography>
The minigames you play are randomly selected. It might take you few tries to get used to them.
</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}>
</ul>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", width: "100%" }}>
<Button onClick={props.start}>Start</Button>
</Grid>
<Grid item xs={3}>
<Button onClick={props.cancel}>Cancel</Button>
</Grid>
</Grid>
</>
</Box>
</Paper>
</Container>
);
}

@ -1,14 +1,16 @@
import React, { useState, useEffect } 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 { Close, Flag, Report } from "@mui/icons-material";
import { Box, Paper, Typography } from "@mui/material";
import { uniqueId } from "lodash";
import React, { useEffect, useState } from "react";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
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 {
[key: string]: number;
@ -81,32 +83,77 @@ export function MinesweeperGame(props: IMinigameProps): React.ReactElement {
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 (
<Grid container spacing={3}>
<>
<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>
{minefield.map((line, y) => (
<div key={y}>
<Typography>
{line.map((cell, x) => {
if (memoryPhase) {
if (minefield[y][x]) return <span key={x}>[?]&nbsp;</span>;
return <span key={x}>[&nbsp;]&nbsp;</span>;
} else {
if (x == pos[0] && y == pos[1]) return <span key={x}>[X]&nbsp;</span>;
if (answer[y][x]) return <span key={x}>[.]&nbsp;</span>;
if (hasAugment && minefield[y][x]) return <span key={x}>[?]&nbsp;</span>;
return <span key={x}>[&nbsp;]&nbsp;</span>;
}
})}
</Typography>
<br />
</div>
))}
<Box
sx={{
display: "grid",
gridTemplateColumns: `repeat(${Math.round(difficulty.width)}, 1fr)`,
gridTemplateRows: `repeat(${Math.round(difficulty.height)}, 1fr)`,
gap: 1,
}}
>
{flatGrid.map((item) => {
let color: string;
let icon: React.ReactElement;
if (item.marked) {
color = Settings.theme.warning;
icon = <Flag />;
} else if (item.current) {
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} />
</Grid>
</Grid>
</Paper>
</>
);
}

@ -1,13 +1,12 @@
import React, { useState, useEffect } from "react";
import Grid from "@mui/material/Grid";
import { Box, Paper, Typography } from "@mui/material";
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 { 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 {
[key: string]: number;
@ -59,23 +58,25 @@ export function SlashGame(props: IMinigameProps): React.ReactElement {
}, []);
return (
<Grid container spacing={3}>
<>
<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>
{hasAugment ? (
<>
<Typography variant="h4">Guard will drop in...</Typography>
<GameTimer millis={timeUntilAttacking} onExpire={props.onFailure} />
</>
<Box sx={{ my: 1 }}>
<Typography variant="h5">Guard will drop in...</Typography>
<GameTimer millis={timeUntilAttacking} onExpire={() => null} noPaper />
</Box>
) : (
<></>
)}
{phase === 0 && <Typography variant="h4">Guarding ...</Typography>}
{phase === 1 && <Typography variant="h4">Preparing?</Typography>}
{phase === 2 && <Typography variant="h4">ATTACKING!</Typography>}
<KeyHandler onKeyDown={press} onFailure={props.onFailure} />
</Grid>
</Grid>
</Paper>
</>
);
}

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

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

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

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

@ -1233,16 +1233,21 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
return false;
}
},
killall: function (_hostname: unknown = workerScript.hostname): boolean {
killall: function (_hostname: unknown = workerScript.hostname, _safetyguard: unknown = true): boolean {
updateDynamicRam("killall", getRamCost(Player, "killall"));
const hostname = helper.string("killall", "hostname", _hostname);
const safetyguard = helper.boolean(_safetyguard);
if (hostname === undefined) {
throw makeRuntimeErrorMsg("killall", "Takes 1 argument");
throw makeRuntimeErrorMsg("killall", "Usage: killall(hostname, [safetyguard boolean])");
}
const server = safeGetServer(hostname, "killall");
const scriptsRunning = server.runningScripts.length > 0;
let scriptsKilled = 0;
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);
++scriptsKilled;
}
WorkerScriptStartStopEventEmitter.emit();
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.`,
);
return scriptsRunning;
return scriptsKilled > 0;
},
exit: function (): void {
updateDynamicRam("exit", getRamCost(Player, "exit"));

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

@ -342,7 +342,7 @@ export function NetscriptGang(player: IPlayer, workerScript: WorkerScript, helpe
checkGangApiAccess("getBonusTime");
const gang = player.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 { CityName } from "../Locations/data/CityNames";
import { getRamCost } from "../Netscript/RamCostGenerator";
@ -28,10 +28,10 @@ export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, h
updateRam("getAugmentationGraftPrice");
const augName = helper.string("getAugmentationGraftPrice", "augName", _augName);
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}`);
}
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
const graftableAug = new GraftableAugmentation(StaticAugmentations[augName]);
return graftableAug.cost;
},
@ -39,10 +39,10 @@ export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, h
updateRam("getAugmentationGraftTime");
const augName = helper.string("getAugmentationGraftTime", "augName", _augName);
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}`);
}
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
const graftableAug = new GraftableAugmentation(StaticAugmentations[augName]);
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.",
);
}
if (!getGraftingAvailableAugs(player).includes(augName) || !Augmentations.hasOwnProperty(augName)) {
if (!getGraftingAvailableAugs(player).includes(augName) || !StaticAugmentations.hasOwnProperty(augName)) {
workerScript.log("grafting.graftAugmentation", () => `Invalid aug: ${augName}`);
return false;
}
@ -75,7 +75,7 @@ export function NetscriptGrafting(player: IPlayer, workerScript: WorkerScript, h
workerScript.log("graftAugmentation", () => txt);
}
const craftableAug = new GraftableAugmentation(Augmentations[augName]);
const craftableAug = new GraftableAugmentation(StaticAugmentations[augName]);
if (player.money < craftableAug.cost) {
workerScript.log("grafting.graftAugmentation", () => `You don't have enough money to craft ${augName}`);
return false;

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

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

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

@ -1,11 +1,11 @@
import { Augmentations } from "../../Augmentation/Augmentations";
import { StaticAugmentations } from "../../Augmentation/StaticAugmentations";
import { GraftableAugmentation } from "./GraftableAugmentation";
import { IPlayer } from "../IPlayer";
export const getGraftingAvailableAugs = (player: IPlayer): string[] => {
const augs: string[] = [];
for (const [augName, aug] of Object.entries(Augmentations)) {
for (const [augName, aug] of Object.entries(StaticAugmentations)) {
if (aug.isSpecial) continue;
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 React, { useState, useEffect } from "react";
import { Augmentation } from "../../../Augmentation/Augmentation";
import { Augmentations } from "../../../Augmentation/Augmentations";
import { StaticAugmentations } from "../../../Augmentation/StaticAugmentations";
import { AugmentationNames } from "../../../Augmentation/data/AugmentationNames";
import { CONSTANTS } from "../../../Constants";
import { hasAugmentationPrereqs } from "../../../Faction/FactionHelpers";
@ -54,7 +54,7 @@ export const GraftingRoot = (): React.ReactElement => {
const player = use.Player();
const router = use.Router();
for (const aug of Object.values(Augmentations)) {
for (const aug of Object.values(StaticAugmentations)) {
const name = aug.name;
const graftableAug = new GraftableAugmentation(aug);
GraftableAugmentations[name] = graftableAug;
@ -62,6 +62,7 @@ export const GraftingRoot = (): React.ReactElement => {
const [selectedAug, setSelectedAug] = useState(getGraftingAvailableAugs(player)[0]);
const [graftOpen, setGraftOpen] = useState(false);
const selectedAugmentation = StaticAugmentations[selectedAug];
const setRerender = useState(false)[1];
function rerender(): void {
@ -148,22 +149,26 @@ export const GraftingRoot = (): React.ReactElement => {
{/* Use formula so the displayed creation time is accurate to player bonus */}
</Typography>
{Augmentations[selectedAug].prereqs.length > 0 && (
<AugPreReqsChecklist player={player} aug={Augmentations[selectedAug]} />
{selectedAugmentation.prereqs.length > 0 && (
<AugPreReqsChecklist player={player} aug={selectedAugmentation} />
)}
<br />
<Typography>
{(() => {
const aug = Augmentations[selectedAug];
const info = typeof aug.info === "string" ? <span>{aug.info}</span> : aug.info;
const info =
typeof selectedAugmentation.info === "string" ? (
<span>{selectedAugmentation.info}</span>
) : (
selectedAugmentation.info
);
const tooltip = (
<>
{info}
<br />
<br />
{aug.stats}
{selectedAugmentation.stats}
</>
);
return tooltip;

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

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

@ -1,6 +1,5 @@
import { IPlayer } from "../IPlayer";
import { PlayerObject } from "./PlayerObject";
import { Augmentations } from "../../Augmentation/Augmentations";
import { applyAugmentation } from "../../Augmentation/AugmentationHelpers";
import { PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
@ -1364,10 +1363,10 @@ export function craftAugmentationWork(this: IPlayer, numCycles: number): boolean
return false;
}
export function finishGraftAugmentationWork(this: IPlayer, cancelled: boolean): string {
export function finishGraftAugmentationWork(this: IPlayer, cancelled: boolean, singularity = false): string {
const augName = this.graftAugmentationName;
if (cancelled === false) {
applyAugmentation(Augmentations[augName]);
applyAugmentation({ name: augName, level: 1 });
if (!this.hasAugmentation(AugmentationNames.CongruityImplant)) {
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` +
(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.`);
}
@ -1700,6 +1699,9 @@ export function singularityStopWork(this: IPlayer): string {
case CONSTANTS.WorkTypeCrime:
res = this.finishCrime(true);
break;
case CONSTANTS.WorkTypeGraftAugmentation:
res = this.finishGraftAugmentationWork(true, true);
break;
default:
console.error(`Unrecognized work type (${this.workType})`);
return "";
@ -1748,14 +1750,6 @@ export function hospitalize(this: IPlayer): number {
//The 'sing' argument designates whether or not this is being called from
//the applyToCompany() Netscript Singularity function
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
if (!(company instanceof Company)) {
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;
if (!this.isQualified(company, pos)) {
const reqText = getJobRequirementText(company, pos);
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;
}
// Check if this company has the position
if (!company.hasPosition(pos)) {
console.error(`Company ${company.name} does not have position ${pos}. Player.applyToCompany() failed`);
return false;
}
while (true) {
const newPos = getNextCompanyPositionHelper(pos);
if (newPos == null) {
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;
}
const nextPos = getNextCompanyPositionHelper(pos);
if (nextPos == null) break;
if (company.hasPosition(nextPos) && this.isQualified(company, nextPos)) {
pos = nextPos;
} else break;
}
//Check if the determined job is the same as the player's current job
if (currCompany != null) {
if (currCompany.name == company.name && pos.name == currPositionName) {
//Check if player already has the assigned job
if (this.jobs[company.name] === pos.name) {
if (!sing) {
const nextPos = getNextCompanyPositionHelper(pos);
if (nextPos == null) {
if (!sing) {
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;
if (nextPos == null || !company.hasPosition(nextPos)) {
dialogBoxCreate("You are already at the highest position for your field! No promotion available");
} else {
if (!sing) {
dialogBoxCreate("You are already at the highest position for your field! No promotion available");
}
return false;
const reqText = getJobRequirementText(company, nextPos);
dialogBoxCreate("Unfortunately, you do not qualify for a promotion<br>" + reqText);
}
}
return false;
}
this.jobs[company.name] = pos.name;
if (!this.focus && this.isWorking && this.companyName !== this.location) this.resetWorkStatus();
this.companyName = this.location;
if (!this.isWorking || !this.workType.includes("Working for Company")) this.companyName = company.name;
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;
}
@ -1869,7 +1841,7 @@ export function getNextCompanyPosition(
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) {
this.finishWork(true);
}

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

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

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

@ -77,7 +77,9 @@ function possibleFactions(player: IPlayer, sleeve: Sleeve): string[] {
}
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;
});
}

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

@ -1800,6 +1800,18 @@ export interface Singularity {
*/
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.
* @remarks
@ -2340,13 +2352,13 @@ export interface Singularity {
* @example
* ```ts
* // NS1
* getDarkwebProgramsAvailable();
* getDarkwebPrograms();
* // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc]
* ```
* @example
* ```ts
* // NS2
* ns.getDarkwebProgramsAvailable();
* ns.getDarkwebPrograms();
* // returns ['BruteSSH.exe', 'FTPCrack.exe'...etc]
* ```
* @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.
*
* @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.
*/
killall(host?: string): boolean;
killall(host?: string, safetyguard?: boolean): boolean;
/**
* Terminates the current script immediately.
@ -6126,7 +6139,7 @@ export interface NS {
* Returns 0 if the script does not exist.
*
* @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.
*/
getScriptRam(script: string, host?: string): number;
@ -7044,22 +7057,27 @@ interface CorporationInfo {
interface Employee {
/** Name of the employee */
name: string;
/** Morale */
/** Morale of the employee */
mor: number;
/** Happiness */
/** Happiness of the employee */
hap: number;
/** Energy */
/** Energy of the employee */
ene: number;
/** Intelligence of the employee */
int: number;
/** Charisma of the employee */
cha: number;
/** Experience of the employee */
exp: number;
/** Creativity of the employee */
cre: number;
/** Efficiency of the employee */
eff: number;
/** Salary */
/** Salary of the employee */
sal: number;
/** City */
/** Current Location (city) */
loc: string;
/** Current job */
/** Current job position */
pos: string;
}
@ -7111,6 +7129,8 @@ interface Material {
sell: number;
/** cost to buy material */
cost: number;
/** Sell cost, can be "MP+5" */
sCost: string | number;
}
/**
@ -7202,6 +7222,8 @@ interface Division {
cities: string[];
/** Products developed by this division */
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.
if (ITutorial.isRunning && ITutorial.currStep === iTutorialSteps.TerminalTypeScript) {
//Make sure filename + code properly follow tutorial
if (currentScript.fileName !== "n00dles.script") {
dialogBoxCreate("Leave the script name as 'n00dles.script'!");
if (currentScript.fileName !== "n00dles.script" && currentScript.fileName !== "n00dles.js") {
dialogBoxCreate("Don't change the script name for now.");
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!");
return;
}
@ -692,7 +695,7 @@ export function Root(props: IProps): React.ReactElement {
const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName);
if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex as number].code) {
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) => {
if (result) {
// Save changes
@ -855,22 +858,31 @@ export function Root(props: IProps): React.ReactElement {
)}
</Tooltip>
{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 = {
maxWidth: `${tabIconWidth}px`,
minWidth: `${tabIconWidth}px`,
minHeight: "38.5px",
maxHeight: "38.5px",
...(currentScript?.fileName === filteredOpenScripts[index].fileName
? {
background: Settings.theme.button,
borderColor: Settings.theme.button,
color: Settings.theme.primary,
}
: {
background: Settings.theme.backgroundsecondary,
borderColor: Settings.theme.backgroundsecondary,
color: Settings.theme.secondary,
}),
...colorProps,
};
const scriptTabText = `${hostname}:~/${fileName} ${dirty(index)}`;
@ -905,17 +917,7 @@ export function Root(props: IProps): React.ReactElement {
maxWidth: `${tabTextWidth}px`,
minHeight: "38.5px",
overflow: "hidden",
...(currentScript?.fileName === filteredOpenScripts[index].fileName
? {
background: Settings.theme.button,
borderColor: Settings.theme.button,
color: Settings.theme.primary,
}
: {
background: Settings.theme.backgroundsecondary,
borderColor: Settings.theme.backgroundsecondary,
color: Settings.theme.secondary,
}),
...colorProps,
}}
>
<span style={{ overflow: "hidden", direction: "rtl", textOverflow: "ellipsis" }}>

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { KEY } from "../../utils/helpers/keyCodes";
import { KEYCODE } from "../../utils/helpers/keyCodes";
import clsx from "clsx";
import { styled, Theme, CSSObject } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
@ -275,57 +275,53 @@ export function SidebarRoot(props: IProps): React.ReactElement {
function handleShortcuts(this: Document, event: KeyboardEvent): any {
if (Settings.DisableHotkeys) 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();
clickTerminal();
} else if (event.key === KEY.C && event.altKey) {
} else if (event.code === KEYCODE.C && event.altKey) {
event.preventDefault();
clickStats();
} else if (event.key === KEY.E && event.altKey) {
} else if (event.code === KEYCODE.E && event.altKey) {
event.preventDefault();
clickScriptEditor();
} else if (event.key === KEY.S && event.altKey) {
} else if (event.code === KEYCODE.S && event.altKey) {
event.preventDefault();
clickActiveScripts();
} else if (event.key === KEY.H && event.altKey) {
} else if (event.code === KEYCODE.H && event.altKey) {
event.preventDefault();
clickHacknet();
} else if (event.key === KEY.W && event.altKey) {
} else if (event.code === KEYCODE.W && event.altKey) {
event.preventDefault();
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
event.preventDefault();
clickJob();
} else if (event.key === KEY.R && event.altKey) {
} else if (event.code === KEYCODE.R && event.altKey) {
event.preventDefault();
clickTravel();
} else if (event.key === KEY.P && event.altKey) {
} else if (event.code === KEYCODE.P && event.altKey) {
event.preventDefault();
clickCreateProgram();
} else if (event.key === KEY.F && event.altKey) {
} else if (event.code === KEYCODE.F && event.altKey) {
if (props.page == Page.Terminal && Settings.EnableBashHotkeys) {
return;
}
event.preventDefault();
clickFactions();
} else if (event.key === KEY.A && event.altKey) {
} else if (event.code === KEYCODE.A && event.altKey) {
event.preventDefault();
clickAugmentations();
} else if (event.key === KEY.U && event.altKey) {
} else if (event.code === KEYCODE.U && event.altKey) {
event.preventDefault();
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();
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();
clickGang();
}
// if (event.key === KEY.O && event.altKey) {
// event.preventDefault();
// gameOptionsBoxOpen();
// }
}
document.addEventListener("keydown", handleShortcuts);

@ -718,7 +718,11 @@ export class Terminal implements ITerminal {
}
break;
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();
} else {
this.error("Bad command. Please follow the tutorial");
@ -734,7 +738,11 @@ export class Terminal implements ITerminal {
}
break;
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();
} else {
this.error("Bad command. Please follow the tutorial");
@ -742,7 +750,11 @@ export class Terminal implements ITerminal {
}
break;
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();
} else {
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 TextField from "@mui/material/TextField";
import { KEY } from "../../utils/helpers/keyCodes";
import { KEY, KEYCODE } from "../../utils/helpers/keyCodes";
import { ITerminal } from "../ITerminal";
import { IRouter } from "../../ui/Router";
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
if (Settings.EnableBashHotkeys) {
if (event.key === KEY.A && event.ctrlKey) {
if (event.code === KEYCODE.A && event.ctrlKey) {
event.preventDefault();
moveTextCursor("home");
}
if (event.key === KEY.E && event.ctrlKey) {
if (event.code === KEYCODE.E && event.ctrlKey) {
event.preventDefault();
moveTextCursor("end");
}
if (event.key === KEY.B && event.ctrlKey) {
if (event.code === KEYCODE.B && event.ctrlKey) {
event.preventDefault();
moveTextCursor("prevchar");
}
if (event.key === KEY.B && event.altKey) {
if (event.code === KEYCODE.B && event.altKey) {
event.preventDefault();
moveTextCursor("prevword");
}
if (event.key === KEY.F && event.ctrlKey) {
if (event.code === KEYCODE.F && event.ctrlKey) {
event.preventDefault();
moveTextCursor("nextchar");
}
if (event.key === KEY.F && event.altKey) {
if (event.code === KEYCODE.F && event.altKey) {
event.preventDefault();
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");
event.preventDefault();
}
if (event.key === KEY.W && event.ctrlKey) {
if (event.code === KEYCODE.W && event.ctrlKey) {
event.preventDefault();
modifyInput("deletewordbefore");
}
if (event.key === KEY.D && event.altKey) {
if (event.code === KEYCODE.D && event.altKey) {
event.preventDefault();
modifyInput("deletewordafter");
}
if (event.key === KEY.U && event.ctrlKey) {
if (event.code === KEYCODE.U && event.ctrlKey) {
event.preventDefault();
modifyInput("clearbefore");
}
if (event.key === KEY.K && event.ctrlKey) {
if (event.code === KEYCODE.K && event.ctrlKey) {
event.preventDefault();
modifyInput("clearafter");
}

@ -134,20 +134,191 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
}, []);
function lineClass(s: string): string {
if (s === "error") {
return classes.error;
}
if (s === "success") {
return classes.success;
}
if (s === "info") {
return classes.info;
}
if (s === "warn") {
return classes.warning;
const lineClassMap: Record<string, string> = {
error: classes.error,
success: classes.success,
info: classes.info,
warn: classes.warning,
};
return lineClassMap[s] || classes.primary;
}
function ansiCodeStyle(code: string | null): Record<string, any> {
// 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();
@ -160,7 +331,7 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
return (
<ListItem key={i} classes={{ root: classes.nopadding }}>
<Typography classes={{ root: lineClass(item.color) }} paragraph={false}>
{item.text}
{parseOutputText(item)}
</Typography>
</ListItem>
);

@ -1532,7 +1532,8 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
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",
`&nbsp; &nbsp; ${compressed}\n`,
"Decode it and output the original string.\n\n",
"Example: decoding '5aaabc340533bca' chunk-by-chunk\n",
"&nbsp; &nbsp; 5aaabc &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabc\n",
"&nbsp; &nbsp; 5aaabc34 &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabcaab\n",
"&nbsp; &nbsp; 5aaabc340 &nbsp; &nbsp; &nbsp; &nbsp;-> &nbsp;aaabcaab\n",
"&nbsp; &nbsp; 5aaabc34053 &nbsp; &nbsp; &nbsp;-> &nbsp;aaabcaabaabaa\n",
"&nbsp; &nbsp; 5aaabc340533bca &nbsp;-> &nbsp;aaabcaabaabaabca",
"Example: decoding '5aaabb450723abb' chunk-by-chunk\n",
"&nbsp; &nbsp; 5aaabb &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabb\n",
"&nbsp; &nbsp; 5aaabb45 &nbsp; &nbsp; &nbsp; &nbsp; -> &nbsp;aaabbaaab\n",
"&nbsp; &nbsp; 5aaabb450 &nbsp; &nbsp; &nbsp; &nbsp;-> &nbsp;aaabbaaab\n",
"&nbsp; &nbsp; 5aaabb45072 &nbsp; &nbsp; &nbsp;-> &nbsp;aaabbaaababababa\n",
"&nbsp; &nbsp; 5aaabb450723abb &nbsp;-> &nbsp;aaabbaaababababaabb",
].join(" ");
},
gen: (): string => {
@ -1591,21 +1592,21 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
`&nbsp; &nbsp; ${plaintext}\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",
"&nbsp; &nbsp; abracadabra &nbsp; &nbsp;-> &nbsp;7abracad47\n",
"&nbsp; &nbsp; mississippi &nbsp; &nbsp;-> &nbsp;4miss433ppi\n",
"&nbsp; &nbsp; aAAaAAaAaAA &nbsp; &nbsp;-> &nbsp;3aAA53035\n",
"&nbsp; &nbsp; 2718281828 &nbsp; &nbsp; -> &nbsp;627182844\n",
"&nbsp; &nbsp; abcdefghijk &nbsp; &nbsp;-> &nbsp;9abcdefghi02jk\n",
"&nbsp; &nbsp; aaaaaaaaaaa &nbsp; &nbsp;-> &nbsp;1a911a\n",
"&nbsp; &nbsp; aaaaaaaaaaaa &nbsp; -> &nbsp;1a912aa\n",
"&nbsp; &nbsp; aaaaaaaaaaaaa &nbsp;-> &nbsp;1a91031",
"&nbsp; &nbsp; abracadabra &nbsp; &nbsp; -> &nbsp;7abracad47\n",
"&nbsp; &nbsp; mississippi &nbsp; &nbsp; -> &nbsp;4miss433ppi\n",
"&nbsp; &nbsp; aAAaAAaAaAA &nbsp; &nbsp; -> &nbsp;3aAA53035\n",
"&nbsp; &nbsp; 2718281828 &nbsp; &nbsp; &nbsp;-> &nbsp;627182844\n",
"&nbsp; &nbsp; abcdefghijk &nbsp; &nbsp; -> &nbsp;9abcdefghi02jk\n",
"&nbsp; &nbsp; aaaaaaaaaaaa &nbsp; &nbsp;-> &nbsp;3aaa91\n",
"&nbsp; &nbsp; aaaaaaaaaaaaa &nbsp; -> &nbsp;1a91031\n",
"&nbsp; &nbsp; aaaaaaaaaaaaaa &nbsp;-> &nbsp;1a91041",
].join(" ");
},
gen: (): string => {
return comprLZGenerate();
},
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 { LoadingScreen } from "./ui/LoadingScreen";
import { initElectron } from "./Electron";
import { AlertEvents } from "./ui/React/AlertManager";
initElectron();
globalThis["React"] = React;
globalThis["ReactDOM"] = ReactDOM;

@ -9,6 +9,7 @@ import ArrowBackIos from "@mui/icons-material/ArrowBackIos";
import { ITutorialEvents } from "./ITutorialEvents";
import { CopyableText } from "../React/CopyableText";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import EqualizerIcon from "@mui/icons-material/Equalizer";
import LastPageIcon from "@mui/icons-material/LastPage";
@ -27,6 +28,7 @@ import {
iTutorialSteps,
iTutorialEnd,
} from "../../InteractiveTutorial";
import { NSSelection } from "./NSSelection";
interface IContent {
content: React.ReactElement;
@ -45,9 +47,23 @@ const useStyles = makeStyles((theme: Theme) =>
}),
);
enum Language {
None,
NS1,
NS2,
}
export function InteractiveTutorialRoot(): React.ReactElement {
const [nsSelectionOpen, setNSSelectionOpen] = useState(false);
const [language, setLanguage] = useState(Language.None);
const classes = useStyles();
const tutorialScriptName = {
[Language.None]: "n00dles.script",
[Language.NS1]: "n00dles.script",
[Language.NS2]: "n00dles.js",
}[language];
const contents: { [number: string]: IContent | undefined } = {
[iTutorialSteps.Start as number]: {
content: (
@ -66,6 +82,47 @@ export function InteractiveTutorialRoot(): React.ReactElement {
),
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]: {
content: (
<>
@ -321,7 +378,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
<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 classes={{ root: classes.textfield }}>{"[home ~/]> nano n00dles.script"}</Typography>
<Typography classes={{ root: classes.textfield }}>{`[home ~/]> nano ${tutorialScriptName}`}</Typography>
<Typography>
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: (
<>
<Typography>
This is the script editor. You can use it to program your scripts. Scripts are written in a simplified
version of javascript. Copy and paste the following code into the script editor: <br />
This is the script editor. You can use it to program your scripts.{" "}
{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 classes={{ root: classes.code }}>
<CopyableText
value={`while(true) {
{language !== Language.NS2 && (
<CopyableText
value={`while(true) {
hack('n00dles');
}`}
/>
/>
)}
{language === Language.NS2 && (
<CopyableText
value={`export async function main(ns) {
while(true) {
await ns.hack('n00dles');
}
}`}
/>
)}
</Typography>
<Typography>
For anyone with basic programming experience, this code should be straightforward. This script will
@ -378,7 +447,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
<Typography>
We have 8GB of free RAM on this machine, which is enough to run our script. Let's run our script using
</Typography>
<Typography classes={{ root: classes.textfield }}>{"[home ~/]> run n00dles.script"}</Typography>
<Typography classes={{ root: classes.textfield }}>{`[home ~/]> run ${tutorialScriptName}`}</Typography>
</>
),
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
these logs using the tail command. Do that now for the script we just ran by typing{" "}
</Typography>
<Typography classes={{ root: classes.textfield }}>{"[home ~/]> tail n00dles.script"}</Typography>
<Typography classes={{ root: classes.textfield }}>{`[home ~/]> tail ${tutorialScriptName}`}</Typography>
</>
),
canNext: false,
@ -447,14 +516,6 @@ export function InteractiveTutorialRoot(): React.ReactElement {
in the main navigation menu to look at the documentation.
<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!
</Typography>
</>
@ -549,25 +610,30 @@ export function InteractiveTutorialRoot(): React.ReactElement {
const content = contents[step];
if (content === undefined) throw new Error("error in the tutorial");
return (
<Paper square sx={{ maxWidth: "70vw", p: 2 }}>
{content.content}
{step !== iTutorialSteps.TutorialPageInfo && (
<>
<IconButton onClick={iTutorialPrevStep} aria-label="previous">
<ArrowBackIos />
</IconButton>
{content.canNext && (
<IconButton onClick={iTutorialNextStep} aria-label="next">
<ArrowForwardIos />
</IconButton>
)}
</>
)}
<br />
<br />
<Button onClick={iTutorialEnd}>
{step !== iTutorialSteps.TutorialPageInfo ? "SKIP TUTORIAL" : "FINISH TUTORIAL"}
</Button>
</Paper>
<>
<NSSelection open={nsSelectionOpen} onClose={() => setNSSelectionOpen(false)} />
<Paper square sx={{ maxWidth: "70vw", p: 2 }}>
{content.content}
{step !== iTutorialSteps.TutorialPageInfo && (
<>
{step !== iTutorialSteps.Start && (
<IconButton onClick={iTutorialPrevStep} aria-label="previous">
<ArrowBackIos />
</IconButton>
)}
{content.canNext && (
<IconButton onClick={iTutorialNextStep} aria-label="next">
<ArrowForwardIos />
</IconButton>
)}
</>
)}
<br />
<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",
alignItems: "center",
justifyContent: "center",
zIndex: 999999,
},
paper: {
backgroundColor: theme.palette.background.default,

@ -105,6 +105,13 @@ export function comprLZEncode(plain: string): string {
// start new literal
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 " +
"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 " +
"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 {
//SHIFT: 16, // Check by `&& event.shiftKey`
@ -70,3 +70,69 @@ export enum KEY {
Y = "y",
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",
}