Merge branch 'dev' into add-ns-getRecentScripts
@@ -1,2 +1,8 @@
|
||||
// Defined by webpack on startup or compilation
|
||||
declare let __COMMIT_HASH__: string;
|
||||
|
||||
// When using file-loader, we'll get a path to the resource
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
@@ -2050,7 +2050,7 @@ function initAugmentations(): void {
|
||||
info:
|
||||
"A brain implant carefully assembled around the synapses, which " +
|
||||
"micromanages the activity and levels of various neuroreceptor " +
|
||||
"chemicals and modulates electrical acvitiy to optimize concentration, " +
|
||||
"chemicals and modulates electrical activity to optimize concentration, " +
|
||||
"allowing the user to multitask much more effectively.",
|
||||
stats: (
|
||||
<>
|
||||
|
||||
@@ -26,7 +26,7 @@ export function InstalledAugmentations(): React.ReactElement {
|
||||
|
||||
if (Settings.OwnedAugmentationsOrder === OwnedAugmentationsOrderSetting.Alphabetically) {
|
||||
sourceAugs.sort((aug1, aug2) => {
|
||||
return aug1.name <= aug2.name ? -1 : 1;
|
||||
return aug1.name.localeCompare(aug2.name);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ function BitNodePortal(props: IPortalProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<span onClick={() => setPortalOpen(true)} className={cssClass}>
|
||||
<span onClick={() => setPortalOpen(true)} className={cssClass} aria-label={`enter-bitnode-${bitNode.number.toString()}`}>
|
||||
<b>O</b>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography";
|
||||
import TextField from "@mui/material/TextField";
|
||||
|
||||
const MAX_BET = 100e6;
|
||||
export const DECK_COUNT = 5; // 5-deck multideck
|
||||
|
||||
enum Result {
|
||||
Pending = "",
|
||||
@@ -45,7 +46,7 @@ export class Blackjack extends Game<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.deck = new Deck(5); // 5-deck multideck
|
||||
this.deck = new Deck(DECK_COUNT);
|
||||
|
||||
const initialBet = 1e6;
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ const strategies: {
|
||||
} = {
|
||||
Red: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
},
|
||||
Black: {
|
||||
match: (n: number): boolean => {
|
||||
if (n === 0) return false;
|
||||
return !redNumbers.includes(n);
|
||||
},
|
||||
payout: 1,
|
||||
@@ -118,12 +118,6 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
const [status, setStatus] = useState<string | JSX.Element>("waiting");
|
||||
const [n, setN] = useState(0);
|
||||
const [lock, setLock] = useState(true);
|
||||
const [strategy, setStrategy] = useState<Strategy>({
|
||||
payout: 0,
|
||||
match: (): boolean => {
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const i = window.setInterval(step, 50);
|
||||
@@ -156,13 +150,12 @@ export function Roulette(props: IProps): React.ReactElement {
|
||||
return `${n}${color}`;
|
||||
}
|
||||
|
||||
function play(s: Strategy): void {
|
||||
function play(strategy: Strategy): void {
|
||||
if (reachedLimit(props.p)) return;
|
||||
|
||||
setCanPlay(false);
|
||||
setLock(false);
|
||||
setStatus("playing");
|
||||
setStrategy(s);
|
||||
|
||||
setTimeout(() => {
|
||||
let n = Math.floor(rng.random() * 37);
|
||||
|
||||
@@ -159,18 +159,18 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
const copy = index.slice();
|
||||
for (let i = 0; i < copy.length; i++) {
|
||||
if (copy[i] === locks[i] && !stoppedOne) continue;
|
||||
copy[i] = (copy[i] + 1) % symbols.length;
|
||||
copy[i] = (copy[i] - 1 >= 0) ? copy[i] - 1 : symbols.length - 1;
|
||||
stoppedOne = true;
|
||||
}
|
||||
|
||||
setIndex(copy);
|
||||
|
||||
if (stoppedOne && copy.every((e, i) => e === locks[i])) {
|
||||
checkWinnings();
|
||||
checkWinnings(getTable(copy, symbols));
|
||||
}
|
||||
}
|
||||
|
||||
function getTable(): string[][] {
|
||||
function getTable(index:number[], symbols:string[]): string[][] {
|
||||
return [
|
||||
[
|
||||
symbols[(index[0] + symbols.length - 1) % symbols.length],
|
||||
@@ -209,8 +209,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
]);
|
||||
}
|
||||
|
||||
function checkWinnings(): void {
|
||||
const t = getTable();
|
||||
function checkWinnings(t:string[][]): void {
|
||||
const getPaylineData = function (payline: number[][]): string[] {
|
||||
const data = [];
|
||||
for (const point of payline) {
|
||||
@@ -267,7 +266,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
setInvestment(investment);
|
||||
}
|
||||
|
||||
const t = getTable();
|
||||
const t = getTable(index, symbols);
|
||||
// prettier-ignore
|
||||
return (
|
||||
<>
|
||||
@@ -288,7 +287,7 @@ export function SlotMachine(props: IProps): React.ReactElement {
|
||||
disabled={!canPlay}
|
||||
>Spin!</Button>)}}
|
||||
/>
|
||||
|
||||
|
||||
<Typography variant="h4">{status}</Typography>
|
||||
<Typography>Pay lines</Typography>
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import { TableCell } from "../../ui/React/Table";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface IProps {
|
||||
office: OfficeSpace;
|
||||
@@ -430,51 +431,46 @@ export function IndustryOffice(props: IProps): React.ReactElement {
|
||||
<Typography>
|
||||
Size: {props.office.employees.length} / {props.office.size} employees
|
||||
</Typography>
|
||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||
<span>
|
||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||
Hire Employee
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<br />
|
||||
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
|
||||
<span>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
|
||||
Upgrade size
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<UpgradeOfficeSizeModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={upgradeOfficeSizeOpen}
|
||||
onClose={() => setUpgradeOfficeSizeOpen(false)}
|
||||
/>
|
||||
|
||||
{!division.hasResearch("AutoPartyManager") && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
||||
>
|
||||
<span>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
||||
Throw Party
|
||||
</Button>
|
||||
</span>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr', width: 'fit-content' }}>
|
||||
<Box sx={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
|
||||
<Tooltip title={<Typography>Automatically hires an employee and gives him/her a random name</Typography>}>
|
||||
<Button disabled={props.office.atCapacity()} onClick={autohireEmployeeButtonOnClick}>
|
||||
Hire Employee
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThrowPartyModal
|
||||
<Tooltip title={<Typography>Upgrade the office's size so that it can hold more employees!</Typography>}>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setUpgradeOfficeSizeOpen(true)}>
|
||||
Upgrade size
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<UpgradeOfficeSizeModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={throwPartyOpen}
|
||||
onClose={() => setThrowPartyOpen(false)}
|
||||
open={upgradeOfficeSizeOpen}
|
||||
onClose={() => setUpgradeOfficeSizeOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<br />
|
||||
{!division.hasResearch("AutoPartyManager") && (
|
||||
<>
|
||||
<Tooltip
|
||||
title={<Typography>Throw an office party to increase your employee's morale and happiness</Typography>}
|
||||
>
|
||||
<Button disabled={corp.funds < 0} onClick={() => setThrowPartyOpen(true)}>
|
||||
Throw Party
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThrowPartyModal
|
||||
rerender={props.rerender}
|
||||
office={props.office}
|
||||
open={throwPartyOpen}
|
||||
onClose={() => setThrowPartyOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
|
||||
</Box>
|
||||
<SwitchButton manualMode={employeeManualAssignMode} switchMode={setEmployeeManualAssignMode} />
|
||||
</Box>
|
||||
{employeeManualAssignMode ? (
|
||||
<ManualManagement rerender={props.rerender} office={props.office} />
|
||||
) : (
|
||||
|
||||
@@ -139,13 +139,13 @@ function WarehouseRoot(props: IProps): React.ReactElement {
|
||||
{numeralWrapper.formatBigNumber(props.warehouse.size)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
|
||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
||||
Upgrade Warehouse Size -
|
||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Button disabled={!canAffordUpgrade} onClick={upgradeWarehouseOnClick}>
|
||||
Upgrade Warehouse Size -
|
||||
<MoneyCost money={sizeUpgradeCost} corp={corp} />
|
||||
</Button>
|
||||
|
||||
<Typography>This industry uses the following equation for its production: </Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<Box display="flex">
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '2fr 1fr', m: '5px' }}>
|
||||
<Box>
|
||||
<Tooltip
|
||||
title={
|
||||
@@ -149,11 +149,10 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box sx={{ "& button": { width: '100%' } }}>
|
||||
<Tooltip
|
||||
title={tutorial ? <Typography>Purchase your required materials to get production started!</Typography> : ""}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
color={tutorial ? "error" : "primary"}
|
||||
onClick={() => setPurchaseMaterialOpen(true)}
|
||||
@@ -161,7 +160,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
>
|
||||
{purchaseButtonText}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<PurchaseMaterialModal
|
||||
mat={mat}
|
||||
@@ -177,7 +175,6 @@ export function MaterialElem(props: IMaterialProps): React.ReactElement {
|
||||
<ExportModal mat={mat} open={exportOpen} onClose={() => setExportOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
|
||||
<Button
|
||||
color={division.prodMats.includes(props.mat.name) && !mat.sllman[0] ? "error" : "primary"}
|
||||
|
||||
@@ -89,19 +89,21 @@ export function Overview({ rerender }: IProps): React.ReactElement {
|
||||
<StatsTable rows={multRows} />
|
||||
<br />
|
||||
<BonusTime />
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
||||
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
||||
helping you get started with managing it.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
||||
</Tooltip>
|
||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||
<BribeButton />
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', width: 'fit-content' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' This is a .lit file
|
||||
that guides you through the beginning of setting up a Corporation and provides some tips/pointers for
|
||||
helping you get started with managing it.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Button onClick={() => corp.getStarterGuide(player)}>Getting Started Guide</Button>
|
||||
</Tooltip>
|
||||
{corp.public ? <PublicButtons rerender={rerender} /> : <PrivateButtons rerender={rerender} />}
|
||||
<BribeButton />
|
||||
</Box>
|
||||
<br />
|
||||
<Upgrades rerender={rerender} />
|
||||
</>
|
||||
@@ -125,11 +127,9 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={<Typography>{findInvestorsTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
||||
Find Investors
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={!fundingAvailable} onClick={() => setFindInvestorsopen(true)}>
|
||||
Find Investors
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
@@ -143,7 +143,6 @@ function PrivateButtons({ rerender }: IPrivateButtonsProps): React.ReactElement
|
||||
</Tooltip>
|
||||
<FindInvestorsModal open={findInvestorsopen} onClose={() => setFindInvestorsopen(false)} rerender={rerender} />
|
||||
<GoPublicModal open={goPublicopen} onClose={() => setGoPublicopen(false)} rerender={rerender} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -201,8 +200,8 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
const sellSharesTooltip = sellSharesOnCd
|
||||
? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown)
|
||||
: "Sell your shares in the company. The money earned from selling your " +
|
||||
"shares goes into your personal account, not the Corporation's. " +
|
||||
"This is one of the only ways to profit from your business venture.";
|
||||
"shares goes into your personal account, not the Corporation's. " +
|
||||
"This is one of the only ways to profit from your business venture.";
|
||||
|
||||
const issueNewSharesOnCd = corp.issueNewSharesCooldown > 0;
|
||||
const issueNewSharesTooltip = issueNewSharesOnCd
|
||||
@@ -212,28 +211,21 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={<Typography>{sellSharesTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
||||
Sell Shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={sellSharesOnCd} onClick={() => setSellSharesOpen(true)}>
|
||||
Sell Shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<SellSharesModal open={sellSharesOpen} onClose={() => setSellSharesOpen(false)} rerender={rerender} />
|
||||
<Tooltip title={<Typography>Buy back shares you that previously issued or sold at market price.</Typography>}>
|
||||
<span>
|
||||
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
|
||||
Buyback shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={corp.issuedShares < 1} onClick={() => setBuybackSharesOpen(true)}>
|
||||
Buyback shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<BuybackSharesModal open={buybackSharesOpen} onClose={() => setBuybackSharesOpen(false)} rerender={rerender} />
|
||||
<br />
|
||||
<Tooltip title={<Typography>{issueNewSharesTooltip}</Typography>}>
|
||||
<span>
|
||||
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
||||
Issue New Shares
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={issueNewSharesOnCd} onClick={() => setIssueNewSharesOpen(true)}>
|
||||
Issue New Shares
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<IssueNewSharesModal open={issueNewSharesOpen} onClose={() => setIssueNewSharesOpen(false)} />
|
||||
<Tooltip
|
||||
@@ -242,7 +234,6 @@ function PublicButtons({ rerender }: IPublicButtonsProps): React.ReactElement {
|
||||
<Button onClick={() => setIssueDividendsOpen(true)}>Issue Dividends</Button>
|
||||
</Tooltip>
|
||||
<IssueDividendsModal open={issueDividendsOpen} onClose={() => setIssueDividendsOpen(false)} />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -269,11 +260,9 @@ function BribeButton(): React.ReactElement {
|
||||
: "Your Corporation is not powerful enough to bribe Faction leaders"
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button disabled={!canBribe} onClick={openBribe}>
|
||||
Bribe Factions
|
||||
</Button>
|
||||
</span>
|
||||
<Button disabled={!canBribe} onClick={openBribe}>
|
||||
Bribe Factions
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<BribeFactionModal open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ProductElem(props: IProductProps): React.ReactElement {
|
||||
);
|
||||
} else if (product.sCost) {
|
||||
if (isString(product.sCost)) {
|
||||
const sCost = (product.sCost as string).replace(/MP/g, product.pCost + "");
|
||||
const sCost = (product.sCost as string).replace(/MP/g, product.pCost + product.rat / product.mku + "");
|
||||
sellButtonText = (
|
||||
<>
|
||||
{sellButtonText} @ <Money money={eval(sCost)} />
|
||||
|
||||
@@ -6,17 +6,18 @@ import { IIndustry } from "../IIndustry";
|
||||
import { Research } from "../Actions";
|
||||
import { Node } from "../ResearchTree";
|
||||
import { ResearchMap } from "../ResearchMap";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Button from "@mui/material/Button";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
|
||||
interface INodeProps {
|
||||
n: Node | null;
|
||||
division: IIndustry;
|
||||
@@ -42,8 +43,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
|
||||
dialogBoxCreate(
|
||||
`Researched ${n.text}. It may take a market cycle ` +
|
||||
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
||||
`the Research apply.`,
|
||||
`(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` +
|
||||
`the Research apply.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,8 +53,8 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
color = "info";
|
||||
}
|
||||
|
||||
const but = (
|
||||
<Box>
|
||||
const wrapInTooltip = (ele: React.ReactElement): React.ReactElement => {
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
@@ -63,12 +64,22 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
{ele}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const but = (
|
||||
<Box>
|
||||
{wrapInTooltip(
|
||||
<span>
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}>
|
||||
{n.text}
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research}
|
||||
style={{ width: '100%', textAlign: 'left', justifyContent: 'unset' }}
|
||||
>
|
||||
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -76,15 +87,25 @@ function Upgrade({ n, division }: INodeProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex">
|
||||
{but}
|
||||
<ListItemButton onClick={() => setOpen((old) => !old)}>
|
||||
<ListItemText />
|
||||
<Box display="flex" sx={{ border: '1px solid ' + Settings.theme.well }}>
|
||||
{wrapInTooltip(
|
||||
<span style={{ width: '100%' }}>
|
||||
<Button color={color} disabled={disabled && !n.researched} onClick={research} sx={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
justifyContent: 'unset',
|
||||
borderColor: Settings.theme.button
|
||||
}}>
|
||||
{n.researched && (<CheckIcon sx={{ mr: 1 }} />)}{n.text}
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
<Button onClick={() => setOpen((old) => !old)} sx={{ borderColor: Settings.theme.button, minWidth: 'fit-content' }}>
|
||||
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
||||
</ListItemButton>
|
||||
</Button>
|
||||
</Box>
|
||||
<Collapse in={open} unmountOnExit>
|
||||
<Box m={4}>
|
||||
<Box m={1}>
|
||||
{n.children.map((m) => (
|
||||
<Upgrade key={m.text} division={division} n={m} />
|
||||
))}
|
||||
@@ -108,7 +129,7 @@ export function ResearchModal(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<Upgrade division={props.industry} n={researchTree.root} />
|
||||
<Typography>
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
Research points: {props.industry.sciResearch.qty.toFixed(3)}
|
||||
<br />
|
||||
Multipliers from research:
|
||||
|
||||
@@ -8,6 +8,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { Adjuster } from "./Adjuster";
|
||||
|
||||
interface IProps {
|
||||
player: IPlayer;
|
||||
@@ -38,6 +39,12 @@ export function Sleeves(props: IProps): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
function sleeveSetStoredCycles(cycles: number): void {
|
||||
for (let i = 0; i < props.player.sleeves.length; ++i) {
|
||||
props.player.sleeves[i].storedCycles = cycles;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
@@ -68,6 +75,18 @@ export function Sleeves(props: IProps): React.ReactElement {
|
||||
<Button onClick={sleeveSyncClearAll}>Clear all</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<Adjuster
|
||||
label="Stored Cycles"
|
||||
placeholder="cycles"
|
||||
tons={() => sleeveSetStoredCycles(10000000)}
|
||||
add={sleeveSetStoredCycles}
|
||||
subtract={sleeveSetStoredCycles}
|
||||
reset={() => sleeveSetStoredCycles(0)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</AccordionDetails>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { Player } from "./Player";
|
||||
import { Router } from "./ui/GameRoot";
|
||||
import { isScriptFilename } from "./Script/isScriptFilename";
|
||||
import { Script } from "./Script/Script";
|
||||
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { SnackbarEvents } from "./ui/React/Snackbar";
|
||||
import { IMap } from "./types";
|
||||
import { IMap, IReturnStatus } from "./types";
|
||||
import { GetServer } from "./Server/AllServers";
|
||||
import { resolve } from "cypress/types/bluebird";
|
||||
import { ImportPlayerData, SaveData, saveObject } from "./SaveObject";
|
||||
import { Settings } from "./Settings/Settings";
|
||||
import { exportScripts } from "./Terminal/commands/download";
|
||||
import { CONSTANTS } from "./Constants";
|
||||
import { hash } from "./hash/hash";
|
||||
|
||||
export function initElectron(): void {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
@@ -14,36 +21,81 @@ export function initElectron(): void {
|
||||
(document as any).achievements = [];
|
||||
initWebserver();
|
||||
initAppNotifier();
|
||||
initSaveFunctions();
|
||||
initElectronBridge();
|
||||
}
|
||||
}
|
||||
|
||||
function initWebserver(): void {
|
||||
(document as any).saveFile = function (filename: string, code: string): string {
|
||||
interface IReturnWebStatus extends IReturnStatus {
|
||||
data?: {
|
||||
[propName: string]: any;
|
||||
};
|
||||
}
|
||||
function normalizeFileName(filename: string): string {
|
||||
filename = filename.replace(/\/\/+/g, "/");
|
||||
filename = removeLeadingSlash(filename);
|
||||
if (filename.includes("/")) {
|
||||
filename = "/" + removeLeadingSlash(filename);
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
(document as any).getFiles = function (): IReturnWebStatus {
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
res: true,
|
||||
data: {
|
||||
files: home.scripts.map((script) => ({
|
||||
filename: script.filename,
|
||||
code: script.code,
|
||||
ramUsage: script.ramUsage,
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
(document as any).deleteFile = function (filename: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
const home = GetServer("home");
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
return home.removeFile(filename);
|
||||
};
|
||||
|
||||
(document as any).saveFile = function (filename: string, code: string): IReturnWebStatus {
|
||||
filename = normalizeFileName(filename);
|
||||
|
||||
code = Buffer.from(code, "base64").toString();
|
||||
const home = GetServer("home");
|
||||
if (home === null) return "'home' server not found.";
|
||||
if (isScriptFilename(filename)) {
|
||||
//If the current script already exists on the server, overwrite it
|
||||
for (let i = 0; i < home.scripts.length; i++) {
|
||||
if (filename == home.scripts[i].filename) {
|
||||
home.scripts[i].saveScript(Player, filename, code, "home", home.scripts);
|
||||
return "written";
|
||||
}
|
||||
}
|
||||
|
||||
//If the current script does NOT exist, create a new one
|
||||
const script = new Script();
|
||||
script.saveScript(Player, filename, code, "home", home.scripts);
|
||||
home.scripts.push(script);
|
||||
return "written";
|
||||
if (home === null) {
|
||||
return {
|
||||
res: false,
|
||||
msg: "Home server does not exist.",
|
||||
};
|
||||
}
|
||||
|
||||
return "not a script file";
|
||||
const { success, overwritten } = home.writeToScriptFile(Player, filename, code);
|
||||
let script;
|
||||
if (success) {
|
||||
script = home.getScript(filename);
|
||||
}
|
||||
return {
|
||||
res: success,
|
||||
data: {
|
||||
overwritten,
|
||||
ramUsage: script?.ramUsage,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,6 +119,123 @@ function initAppNotifier(): void {
|
||||
};
|
||||
|
||||
// Will be consumud by the electron wrapper.
|
||||
// @ts-ignore
|
||||
window.appNotifier = funcs;
|
||||
(window as any).appNotifier = funcs;
|
||||
}
|
||||
|
||||
function initSaveFunctions(): void {
|
||||
const funcs = {
|
||||
triggerSave: (): Promise<void> => saveObject.saveGame(true),
|
||||
triggerGameExport: (): void => {
|
||||
try {
|
||||
saveObject.exportGame();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export game.", "error", 2000);
|
||||
}
|
||||
},
|
||||
triggerScriptsExport: (): void => exportScripts("*", Player.getHomeComputer()),
|
||||
getSaveData: (): { save: string; fileName: string } => {
|
||||
return {
|
||||
save: saveObject.getSaveString(Settings.ExcludeRunningScriptsFromSave),
|
||||
fileName: saveObject.getSaveFileName(),
|
||||
};
|
||||
},
|
||||
getSaveInfo: async (base64save: string): Promise<ImportPlayerData | undefined> => {
|
||||
try {
|
||||
const data = await saveObject.getImportDataFromString(base64save);
|
||||
return data.playerData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
},
|
||||
pushSaveData: (base64save: string, automatic = false): void => Router.toImportSave(base64save, automatic),
|
||||
};
|
||||
|
||||
// Will be consumud by the electron wrapper.
|
||||
(window as any).appSaveFns = funcs;
|
||||
}
|
||||
|
||||
function initElectronBridge(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.receive("get-save-data-request", () => {
|
||||
const data = (window as any).appSaveFns.getSaveData();
|
||||
bridge.send("get-save-data-response", data);
|
||||
});
|
||||
bridge.receive("get-save-info-request", async (save: string) => {
|
||||
const data = await (window as any).appSaveFns.getSaveInfo(save);
|
||||
bridge.send("get-save-info-response", data);
|
||||
});
|
||||
bridge.receive("push-save-request", ({ save, automatic = false }: { save: string; automatic: boolean }) => {
|
||||
(window as any).appSaveFns.pushSaveData(save, automatic);
|
||||
});
|
||||
bridge.receive("trigger-save", () => {
|
||||
return (window as any).appSaveFns
|
||||
.triggerSave()
|
||||
.then(() => {
|
||||
bridge.send("save-completed");
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not save game.", "error", 2000);
|
||||
});
|
||||
});
|
||||
bridge.receive("trigger-game-export", () => {
|
||||
try {
|
||||
(window as any).appSaveFns.triggerGameExport();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export game.", "error", 2000);
|
||||
}
|
||||
});
|
||||
bridge.receive("trigger-scripts-export", () => {
|
||||
try {
|
||||
(window as any).appSaveFns.triggerScriptsExport();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
SnackbarEvents.emit("Could not export scripts.", "error", 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function pushGameSaved(data: SaveData): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-game-saved", data);
|
||||
}
|
||||
|
||||
export function pushGameReady(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
// Send basic information to the electron wrapper
|
||||
bridge.send("push-game-ready", {
|
||||
player: {
|
||||
identifier: Player.identifier,
|
||||
playtime: Player.totalPlaytime,
|
||||
lastSave: Player.lastSave,
|
||||
},
|
||||
game: {
|
||||
version: CONSTANTS.VersionString,
|
||||
hash: hash(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function pushImportResult(wasImported: boolean): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-import-result", { wasImported });
|
||||
pushDisableRestore();
|
||||
}
|
||||
|
||||
export function pushDisableRestore(): void {
|
||||
const bridge = (window as any).electronBridge as any;
|
||||
if (!bridge) return;
|
||||
|
||||
bridge.send("push-disable-restore", { duration: 1000 * 60 });
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function DonateOption(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={{ my: 1, p: 1, width: "100%" }}>
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
<Status />
|
||||
{props.disabled ? (
|
||||
<Typography>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Container from "@mui/material/Container";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { Table, TableCell } from "../../ui/React/Table";
|
||||
import { IRouter } from "../../ui/Router";
|
||||
import { Factions } from "../Factions";
|
||||
import { Faction } from "../Faction";
|
||||
import { joinFaction } from "../FactionHelpers";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Box from "@mui/material/Box";
|
||||
import Link from "@mui/material/Link";
|
||||
import Button from "@mui/material/Button";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import { Table, TableCell } from "../../ui/React/Table";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import { Factions } from "../Factions";
|
||||
|
||||
export const InvitationsSeen: string[] = [];
|
||||
|
||||
@@ -48,42 +48,67 @@ export function FactionsRoot(props: IProps): React.ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container disableGutters maxWidth="md" sx={{ mx: 0, mb: 10 }}>
|
||||
<Typography variant="h4">Factions</Typography>
|
||||
<Typography>Lists all factions you have joined</Typography>
|
||||
<br />
|
||||
<Box display="flex" flexDirection="column">
|
||||
{props.player.factions.map((faction: string) => (
|
||||
<Link key={faction} variant="h6" onClick={() => openFaction(Factions[faction])}>
|
||||
{faction}
|
||||
</Link>
|
||||
))}
|
||||
</Box>
|
||||
<br />
|
||||
{props.player.factionInvitations.length > 0 && (
|
||||
<>
|
||||
<Typography variant="h5" color="primary">
|
||||
Outstanding Faction Invitations
|
||||
</Typography>
|
||||
<Typography>
|
||||
Lists factions you have been invited to. You can accept these faction invitations at any time.
|
||||
</Typography>
|
||||
<Table size="small" padding="none">
|
||||
<Typography mb={4}>
|
||||
Throughout the game you may receive invitations from factions. There are many different factions, and each
|
||||
faction has different criteria for determining its potential members. Joining a faction and furthering its cause
|
||||
is crucial to progressing in the game and unlocking endgame content.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" color="primary" mt={2} mb={1}>
|
||||
Factions you have joined:
|
||||
</Typography>
|
||||
{(props.player.factions.length > 0 && (
|
||||
<Paper sx={{ my: 1, p: 1, pb: 0, display: "inline-block" }}>
|
||||
<Table padding="none">
|
||||
<TableBody>
|
||||
{props.player.factionInvitations.map((faction: string) => (
|
||||
{props.player.factions.map((faction: string) => (
|
||||
<TableRow key={faction}>
|
||||
<TableCell>
|
||||
<Typography noWrap>{faction}</Typography>
|
||||
<Typography noWrap mb={1}>
|
||||
{faction}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Button onClick={(e) => acceptInvitation(e, faction)}>Join!</Button>
|
||||
<Box ml={1} mb={1}>
|
||||
<Button onClick={() => openFaction(Factions[faction])}>Details</Button>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</Paper>
|
||||
)) || <Typography>You haven't joined any factions.</Typography>}
|
||||
<Typography variant="h5" color="primary" mt={4} mb={1}>
|
||||
Outstanding Faction Invitations
|
||||
</Typography>
|
||||
<Typography mb={1}>
|
||||
Factions you have been invited to. You can accept these faction invitations at any time:
|
||||
</Typography>
|
||||
{(props.player.factionInvitations.length > 0 && (
|
||||
<Paper sx={{ my: 1, mb: 4, p: 1, pb: 0, display: "inline-block" }}>
|
||||
<Table padding="none">
|
||||
<TableBody>
|
||||
{props.player.factionInvitations.map((faction: string) => (
|
||||
<TableRow key={faction}>
|
||||
<TableCell>
|
||||
<Typography noWrap mb={1}>
|
||||
{faction}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Box ml={1} mb={1}>
|
||||
<Button onClick={(e) => acceptInvitation(e, faction)}>Join!</Button>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
)) || <Typography>You have no outstanding faction invites.</Typography>}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type IProps = {
|
||||
export function Option(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Box>
|
||||
<Paper sx={{ my: 1, p: 1, width: "100%" }}>
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
<Button onClick={props.onClick}>{props.buttonText}</Button>
|
||||
<Typography>{props.infoText}</Typography>
|
||||
</Paper>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
|
||||
props.onAscend();
|
||||
const res = gang.ascendMember(props.member);
|
||||
dialogBoxCreate(
|
||||
<Typography>
|
||||
<>
|
||||
You ascended {props.member.name}!<br />
|
||||
<br />
|
||||
Your gang lost {numeralWrapper.formatRespect(res.respect)} respect.
|
||||
@@ -51,7 +51,7 @@ export function AscensionModal(props: IProps): React.ReactElement {
|
||||
<br />
|
||||
Charisma: x{numeralWrapper.format(res.cha, "0.000")}
|
||||
<br />
|
||||
</Typography>,
|
||||
</>
|
||||
);
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
@@ -2,20 +2,27 @@
|
||||
* React Component for the popup that manages gang members upgrades
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { formatNumber } from "../../utils/StringHelperFunctions";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMemberUpgrades } from "../GangMemberUpgrades";
|
||||
import { GangMemberUpgrade } from "../GangMemberUpgrade";
|
||||
import { Money } from "../../ui/React/Money";
|
||||
import { useGang } from "./Context";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { UpgradeType } from "../data/upgrades";
|
||||
import { use } from "../../ui/Context";
|
||||
import { generateTableRow } from "./GangMemberStats";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Box from "@mui/material/Box";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
import { MenuItem, Table, TableBody, TextField } from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMemberUpgrades } from "../GangMemberUpgrades";
|
||||
import { GangMemberUpgrade } from "../GangMemberUpgrade";
|
||||
import { Money } from "../../ui/React/Money";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { UpgradeType } from "../data/upgrades";
|
||||
import { use } from "../../ui/Context";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview";
|
||||
|
||||
interface INextRevealProps {
|
||||
upgrades: string[];
|
||||
@@ -46,12 +53,10 @@ function NextReveal(props: INextRevealProps): React.ReactElement {
|
||||
function PurchasedUpgrade({ upgName }: { upgName: string }): React.ReactElement {
|
||||
const upg = GangMemberUpgrades[upgName];
|
||||
return (
|
||||
<Paper sx={{ mx: 1, p: 1 }}>
|
||||
<Box display="flex">
|
||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: upg.desc }} />}>
|
||||
<Typography>{upg.name}</Typography>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Paper sx={{ p: 1 }}>
|
||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: upg.desc }} />}>
|
||||
<Typography>{upg.name}</Typography>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -72,8 +77,8 @@ function UpgradeButton(props: IUpgradeButtonProps): React.ReactElement {
|
||||
return (
|
||||
<Tooltip title={<Typography dangerouslySetInnerHTML={{ __html: props.upg.desc }} />}>
|
||||
<span>
|
||||
<Typography>{props.upg.name}</Typography>
|
||||
<Button onClick={onClick}>
|
||||
<Button onClick={onClick} sx={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }}>
|
||||
<Typography sx={{ display: 'block' }}>{props.upg.name}</Typography>
|
||||
<Money money={gang.getUpgradeCost(props.upg)} />
|
||||
</Button>
|
||||
</span>
|
||||
@@ -86,12 +91,16 @@ interface IPanelProps {
|
||||
}
|
||||
|
||||
function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const gang = useGang();
|
||||
const player = use.Player();
|
||||
const setRerender = useState(false)[1];
|
||||
const [currentCategory, setCurrentCategory] = useState("Weapons");
|
||||
|
||||
function rerender(): void {
|
||||
setRerender((old) => !old);
|
||||
}
|
||||
|
||||
function filterUpgrades(list: string[], type: UpgradeType): GangMemberUpgrade[] {
|
||||
return Object.keys(GangMemberUpgrades)
|
||||
.filter((upgName: string) => {
|
||||
@@ -103,12 +112,26 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
})
|
||||
.map((upgName: string) => GangMemberUpgrades[upgName]);
|
||||
}
|
||||
|
||||
const onChange = (event: SelectChangeEvent<string>): void => {
|
||||
setCurrentCategory(event.target.value);
|
||||
rerender()
|
||||
}
|
||||
|
||||
const weaponUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Weapon);
|
||||
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
|
||||
const vehicleUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Vehicle);
|
||||
const rootkitUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Rootkit);
|
||||
const augUpgrades = filterUpgrades(props.member.augmentations, UpgradeType.Augmentation);
|
||||
|
||||
const categories: { [key: string]: (GangMemberUpgrade[] | UpgradeType)[] } = {
|
||||
'Weapons': [weaponUpgrades, UpgradeType.Weapon],
|
||||
'Armor': [armorUpgrades, UpgradeType.Armor],
|
||||
'Vehicles': [vehicleUpgrades, UpgradeType.Vehicle],
|
||||
'Rootkits': [rootkitUpgrades, UpgradeType.Rootkit],
|
||||
'Augmentations': [augUpgrades, UpgradeType.Augmentation]
|
||||
};
|
||||
|
||||
const asc = {
|
||||
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
||||
str: props.member.calculateAscensionMult(props.member.str_asc_points),
|
||||
@@ -119,26 +142,89 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
};
|
||||
return (
|
||||
<Paper>
|
||||
<Typography variant="h5" color="primary">
|
||||
{props.member.name} ({props.member.task})
|
||||
</Typography>
|
||||
<Typography>
|
||||
Hack: {props.member.hack} (x
|
||||
{formatNumber(props.member.hack_mult * asc.hack, 2)})<br />
|
||||
Str: {props.member.str} (x
|
||||
{formatNumber(props.member.str_mult * asc.str, 2)})<br />
|
||||
Def: {props.member.def} (x
|
||||
{formatNumber(props.member.def_mult * asc.def, 2)})<br />
|
||||
Dex: {props.member.dex} (x
|
||||
{formatNumber(props.member.dex_mult * asc.dex, 2)})<br />
|
||||
Agi: {props.member.agi} (x
|
||||
{formatNumber(props.member.agi_mult * asc.agi, 2)})<br />
|
||||
Cha: {props.member.cha} (x
|
||||
{formatNumber(props.member.cha_mult * asc.cha, 2)})
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Typography>Purchased Upgrades: </Typography>
|
||||
<br />
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', m: 1, gap: 1 }}>
|
||||
<span>
|
||||
<Typography variant="h5" color="primary">
|
||||
{props.member.name} ({props.member.task})
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
Hk: x{numeralWrapper.formatMultiplier(props.member.hack_mult * asc.hack)}(x
|
||||
{numeralWrapper.formatMultiplier(props.member.hack_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.hack)}{" "}
|
||||
Asc)
|
||||
<br />
|
||||
St: x{numeralWrapper.formatMultiplier(props.member.str_mult * asc.str)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.str_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.str)}{" "}
|
||||
Asc)
|
||||
<br />
|
||||
Df: x{numeralWrapper.formatMultiplier(props.member.def_mult * asc.def)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.def_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.def)}{" "}
|
||||
Asc)
|
||||
<br />
|
||||
Dx: x{numeralWrapper.formatMultiplier(props.member.dex_mult * asc.dex)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.dex_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.dex)}{" "}
|
||||
Asc)
|
||||
<br />
|
||||
Ag: x{numeralWrapper.formatMultiplier(props.member.agi_mult * asc.agi)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.agi_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.agi)}{" "}
|
||||
Asc)
|
||||
<br />
|
||||
Ch: x{numeralWrapper.formatMultiplier(props.member.cha_mult * asc.cha)}
|
||||
(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.cha)}{" "}
|
||||
Asc)
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{generateTableRow("Hacking", props.member.hack, props.member.hack_exp, Settings.theme.hack, classes)}
|
||||
{generateTableRow("Strength", props.member.str, props.member.str_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Defense", props.member.def, props.member.def_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Dexterity", props.member.dex, props.member.dex_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Agility", props.member.agi, props.member.agi_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Charisma", props.member.cha, props.member.cha_exp, Settings.theme.cha, classes)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tooltip>
|
||||
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<Select onChange={onChange} value={currentCategory} sx={{ width: '100%', mb: 1 }}>
|
||||
{Object.keys(categories).map((k, i) => (
|
||||
<MenuItem key={i + 1} value={k}>
|
||||
<Typography variant="h6">{k}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{(categories[currentCategory][0] as GangMemberUpgrade[]).length === 0 && (
|
||||
<Typography>
|
||||
All upgrades owned!
|
||||
</Typography>
|
||||
)}
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||
{(categories[currentCategory][0] as GangMemberUpgrade[]).map((upg) => (
|
||||
<UpgradeButton
|
||||
key={upg.name}
|
||||
rerender={rerender}
|
||||
member={props.member}
|
||||
upg={upg}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<NextReveal
|
||||
type={categories[currentCategory][1] as UpgradeType}
|
||||
upgrades={props.member.upgrades}
|
||||
/>
|
||||
</Box>
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ mx: 1 }}>Purchased Upgrades: </Typography>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(4, 1fr)', m: 1 }}>
|
||||
{props.member.upgrades.map((upg: string) => (
|
||||
<PurchasedUpgrade key={upg} upgName={upg} />
|
||||
))}
|
||||
@@ -146,59 +232,22 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
|
||||
<PurchasedUpgrade key={upg} upgName={upg} />
|
||||
))}
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="space-around">
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Weapons
|
||||
</Typography>
|
||||
{weaponUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Weapon} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Armor
|
||||
</Typography>
|
||||
{armorUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Armor} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Vehicles
|
||||
</Typography>
|
||||
{vehicleUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Vehicle} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Rootkits
|
||||
</Typography>
|
||||
{rootkitUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Rootkit} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h6" color="primary">
|
||||
Augmentations
|
||||
</Typography>
|
||||
{augUpgrades.map((upg) => (
|
||||
<UpgradeButton key={upg.name} rerender={rerender} member={props.member} upg={upg} />
|
||||
))}
|
||||
<NextReveal type={UpgradeType.Augmentation} upgrades={props.member.upgrades} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Paper >
|
||||
);
|
||||
}
|
||||
|
||||
export function EquipmentsSubpage(): React.ReactElement {
|
||||
const gang = useGang();
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilter(event.target.value.toLowerCase());
|
||||
}
|
||||
|
||||
const members = gang.members
|
||||
.filter((member) => member && member.name.toLowerCase().includes(filter));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
@@ -209,11 +258,26 @@ export function EquipmentsSubpage(): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Discount: -{numeralWrapper.formatPercentage(1 - 1 / gang.getDiscount())}</Typography>
|
||||
<Typography sx={{ m: 1 }}>Discount: -{numeralWrapper.formatPercentage(1 - 1 / gang.getDiscount())}</Typography>
|
||||
</Tooltip>
|
||||
{gang.members.map((member: GangMember) => (
|
||||
<GangMemberUpgradePanel key={member.name} member={member} />
|
||||
))}
|
||||
|
||||
<TextField
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />,
|
||||
spellCheck: false
|
||||
}}
|
||||
placeholder="Filter by member name"
|
||||
sx={{ m: 1, width: '15%' }}
|
||||
/>
|
||||
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: 'fit-content' }}>
|
||||
{members.map((member: GangMember) => (
|
||||
<GangMemberUpgradePanel key={member.name} member={member} />
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* React Component for a gang member on the management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { GangMemberAccordionContent } from "./GangMemberAccordionContent";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import ExpandMore from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLess from "@mui/icons-material/ExpandLess";
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberAccordion(props: IProps): React.ReactElement {
|
||||
const [open, setOpen] = useState(true);
|
||||
return (
|
||||
<Box component={Paper}>
|
||||
<ListItemButton onClick={() => setOpen((old) => !old)}>
|
||||
<ListItemText primary={<Typography>{props.member.name}</Typography>} />
|
||||
{open ? <ExpandLess color="primary" /> : <ExpandMore color="primary" />}
|
||||
</ListItemButton>
|
||||
<Collapse in={open} unmountOnExit>
|
||||
<Box sx={{ mx: 4 }}>
|
||||
<GangMemberAccordionContent member={props.member} />
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* React Component for the content of the accordion of gang members on the
|
||||
* management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMemberStats } from "./GangMemberStats";
|
||||
import { TaskSelector } from "./TaskSelector";
|
||||
import { TaskDescription } from "./TaskDescription";
|
||||
import { GangMember } from "../GangMember";
|
||||
import Grid from "@mui/material/Grid";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberAccordionContent(props: IProps): React.ReactElement {
|
||||
const setRerender = useState(false)[1];
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={4}>
|
||||
<GangMemberStats onAscend={() => setRerender((old) => !old)} member={props.member} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TaskSelector onTaskChange={() => setRerender((old) => !old)} member={props.member} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TaskDescription member={props.member} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* React Component for a gang member on the management subpage.
|
||||
*/
|
||||
import React from "react";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { GangMemberCardContent } from "./GangMemberCardContent";
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberCard(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Box component={Paper} sx={{ width: 'auto' }}>
|
||||
<Box sx={{ m: 1 }}>
|
||||
<ListItemText primary={<b>{props.member.name}</b>} />
|
||||
<GangMemberCardContent member={props.member} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* React Component for the content of the accordion of gang members on the
|
||||
* management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMemberStats } from "./GangMemberStats";
|
||||
import { TaskSelector } from "./TaskSelector";
|
||||
import { AscensionModal } from "./AscensionModal";
|
||||
|
||||
import { Box } from "@mui/system";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import HelpIcon from "@mui/icons-material/Help";
|
||||
|
||||
import { GangMember } from "../GangMember";
|
||||
import { StaticModal } from "../../ui/React/StaticModal";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
}
|
||||
|
||||
export function GangMemberCardContent(props: IProps): React.ReactElement {
|
||||
const setRerender = useState(false)[1];
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [ascendOpen, setAscendOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.member.canAscend() && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', my: 1 }}>
|
||||
<Button onClick={() => setAscendOpen(true)} style={{ flexGrow: 1, borderRightWidth: 0 }}>Ascend</Button>
|
||||
<AscensionModal
|
||||
open={ascendOpen}
|
||||
onClose={() => setAscendOpen(false)}
|
||||
member={props.member}
|
||||
onAscend={() => setRerender((old) => !old)}
|
||||
/>
|
||||
<Button onClick={() => setHelpOpen(true)} style={{ width: 'fit-content', borderLeftWidth: 0 }}>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
<StaticModal open={helpOpen} onClose={() => setHelpOpen(false)}>
|
||||
<Typography>
|
||||
Ascending a Gang Member resets the member's progress and stats in exchange for a permanent boost to their
|
||||
stat multipliers.
|
||||
<br />
|
||||
<br />
|
||||
The additional stat multiplier that the Gang Member gains upon ascension is based on the amount of exp
|
||||
they have.
|
||||
<br />
|
||||
<br />
|
||||
Upon ascension, the member will lose all of its non-Augmentation Equipment and your gang will lose respect
|
||||
equal to the total respect earned by the member.
|
||||
</Typography>
|
||||
</StaticModal>
|
||||
</Box>
|
||||
)}
|
||||
<Box display="grid" sx={{ gridTemplateColumns: '1fr 1fr', width: '100%', gap: 1 }}>
|
||||
<GangMemberStats member={props.member} />
|
||||
<TaskSelector onTaskChange={() => setRerender((old) => !old)} member={props.member} />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,23 +2,63 @@
|
||||
* React Component for the list of gang members on the management subpage.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { GangMemberAccordion } from "./GangMemberAccordion";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { GangMemberCard } from "./GangMemberCard";
|
||||
import { RecruitButton } from "./RecruitButton";
|
||||
import { useGang } from "./Context";
|
||||
|
||||
import { Box, TextField } from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import { GangMember } from "../GangMember";
|
||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
|
||||
export function GangMemberList(): React.ReactElement {
|
||||
const gang = useGang();
|
||||
const setRerender = useState(false)[1];
|
||||
const [filter, setFilter] = useState("");
|
||||
const [ascendOnly, setAscendOnly] = useState(false);
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilter(event.target.value.toLowerCase());
|
||||
}
|
||||
|
||||
const members = gang.members
|
||||
.filter((member) => member && member.name.toLowerCase().includes(filter))
|
||||
.filter((member) => {
|
||||
if (ascendOnly) return member.canAscend();
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RecruitButton onRecruit={() => setRerender((old) => !old)} />
|
||||
<ul>
|
||||
{gang.members.map((member: GangMember) => (
|
||||
<GangMemberAccordion key={member.name} member={member} />
|
||||
<TextField
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />,
|
||||
spellCheck: false
|
||||
}}
|
||||
placeholder="Filter by member name"
|
||||
sx={{ m: 1, width: '15%' }}
|
||||
/>
|
||||
<OptionSwitch
|
||||
checked={ascendOnly}
|
||||
onChange={(newValue) => (setAscendOnly(newValue))}
|
||||
text="Show only ascendable"
|
||||
tooltip={
|
||||
<>
|
||||
Filter the members list by whether or not the member
|
||||
can be ascended.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Box display="grid" sx={{ gridTemplateColumns: 'repeat(2, 1fr)' }}>
|
||||
{members.map((member: GangMember) => (
|
||||
<GangMemberCard key={member.name} member={member} />
|
||||
))}
|
||||
</ul>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,26 +2,53 @@
|
||||
* React Component for the first part of a gang member details.
|
||||
* Contains skills and exp.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { formatNumber } from "../../utils/StringHelperFunctions";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { AscensionModal } from "./AscensionModal";
|
||||
import React from "react";
|
||||
import { useGang } from "./Context";
|
||||
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Button from "@mui/material/Button";
|
||||
import { StaticModal } from "../../ui/React/StaticModal";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import HelpIcon from "@mui/icons-material/Help";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@mui/material";
|
||||
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { formatNumber } from "../../utils/StringHelperFunctions";
|
||||
import { MoneyRate } from "../../ui/React/MoneyRate";
|
||||
import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
onAscend: () => void;
|
||||
}
|
||||
|
||||
export const generateTableRow = (
|
||||
name: string,
|
||||
level: number,
|
||||
exp: number,
|
||||
color: string,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
classes: any
|
||||
): React.ReactElement => {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<Typography style={{ color: color }}>{name}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" classes={{ root: classes.cellNone }}>
|
||||
<Typography style={{ color: color }}>
|
||||
{formatNumber(level, 0)} ({numeralWrapper.formatExp(exp)} exp)
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
|
||||
export function GangMemberStats(props: IProps): React.ReactElement {
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [ascendOpen, setAscendOpen] = useState(false);
|
||||
const classes = useStyles();
|
||||
|
||||
const asc = {
|
||||
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
||||
@@ -32,6 +59,16 @@ export function GangMemberStats(props: IProps): React.ReactElement {
|
||||
cha: props.member.calculateAscensionMult(props.member.cha_asc_points),
|
||||
};
|
||||
|
||||
|
||||
|
||||
const gang = useGang();
|
||||
const data = [
|
||||
[`Money:`, <MoneyRate money={5 * props.member.calculateMoneyGain(gang)} />],
|
||||
[`Respect:`, `${numeralWrapper.formatRespect(5 * props.member.calculateRespectGain(gang))} / sec`],
|
||||
[`Wanted Level:`, `${numeralWrapper.formatWanted(5 * props.member.calculateWantedLevelGain(gang))} / sec`],
|
||||
[`Total Respect:`, `${numeralWrapper.formatRespect(props.member.earnedRespect)}`],
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
@@ -63,50 +100,32 @@ export function GangMemberStats(props: IProps): React.ReactElement {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>
|
||||
Hacking: {formatNumber(props.member.hack, 0)} ({numeralWrapper.formatExp(props.member.hack_exp)} exp)
|
||||
<br />
|
||||
Strength: {formatNumber(props.member.str, 0)} ({numeralWrapper.formatExp(props.member.str_exp)} exp)
|
||||
<br />
|
||||
Defense: {formatNumber(props.member.def, 0)} ({numeralWrapper.formatExp(props.member.def_exp)} exp)
|
||||
<br />
|
||||
Dexterity: {formatNumber(props.member.dex, 0)} ({numeralWrapper.formatExp(props.member.dex_exp)} exp)
|
||||
<br />
|
||||
Agility: {formatNumber(props.member.agi, 0)} ({numeralWrapper.formatExp(props.member.agi_exp)} exp)
|
||||
<br />
|
||||
Charisma: {formatNumber(props.member.cha, 0)} ({numeralWrapper.formatExp(props.member.cha_exp)} exp)
|
||||
<br />
|
||||
</Typography>
|
||||
<Table sx={{ display: 'table', mb: 1, width: '100%' }}>
|
||||
<TableBody>
|
||||
{generateTableRow("Hacking", props.member.hack, props.member.hack_exp, Settings.theme.hack, classes)}
|
||||
{generateTableRow("Strength", props.member.str, props.member.str_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Defense", props.member.def, props.member.def_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Dexterity", props.member.dex, props.member.dex_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Agility", props.member.agi, props.member.agi_exp, Settings.theme.combat, classes)}
|
||||
{generateTableRow("Charisma", props.member.cha, props.member.cha_exp, Settings.theme.cha, classes)}
|
||||
<TableRow>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<br />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{data.map(([a, b]) => (
|
||||
<TableRow key={a.toString() + b.toString()}>
|
||||
<TableCell classes={{ root: classes.cellNone }}>
|
||||
<Typography>{a}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" classes={{ root: classes.cellNone }}>
|
||||
<Typography>{b}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Tooltip>
|
||||
<br />
|
||||
{props.member.canAscend() && (
|
||||
<>
|
||||
<Button onClick={() => setAscendOpen(true)}>Ascend</Button>
|
||||
<AscensionModal
|
||||
open={ascendOpen}
|
||||
onClose={() => setAscendOpen(false)}
|
||||
member={props.member}
|
||||
onAscend={props.onAscend}
|
||||
/>
|
||||
<IconButton onClick={() => setHelpOpen(true)}>
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
<StaticModal open={helpOpen} onClose={() => setHelpOpen(false)}>
|
||||
<Typography>
|
||||
Ascending a Gang Member resets the member's progress and stats in exchange for a permanent boost to their
|
||||
stat multipliers.
|
||||
<br />
|
||||
<br />
|
||||
The additional stat multiplier that the Gang Member gains upon ascension is based on the amount of exp
|
||||
they have.
|
||||
<br />
|
||||
<br />
|
||||
Upon ascension, the member will lose all of its non-Augmentation Equipment and your gang will lose respect
|
||||
equal to the total respect earned by the member.
|
||||
</Typography>
|
||||
</StaticModal>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,18 +24,20 @@ export function RecruitButton(props: IProps): React.ReactElement {
|
||||
if (!gang.canRecruitMember()) {
|
||||
const respect = gang.getRespectNeededToRecruitMember();
|
||||
return (
|
||||
<Box display="flex" alignItems="center">
|
||||
<Button sx={{ mx: 1 }} disabled>
|
||||
<Box display="flex" alignItems="center" sx={{ mx: 1 }}>
|
||||
<Button disabled>
|
||||
Recruit Gang Member
|
||||
</Button>
|
||||
<Typography>{numeralWrapper.formatRespect(respect)} respect needed to recruit next member</Typography>
|
||||
<Typography sx={{ ml: 1 }}>{numeralWrapper.formatRespect(respect)} respect needed to recruit next member</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>Recruit Gang Member</Button>
|
||||
<Box sx={{ mx: 1 }}>
|
||||
<Button onClick={() => setOpen(true)}>Recruit Gang Member</Button>
|
||||
</Box>
|
||||
<RecruitModal open={open} onClose={() => setOpen(false)} onRecruit={props.onRecruit} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
* the task selector as well as some stats.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { numeralWrapper } from "../../ui/numeralFormat";
|
||||
import { StatsTable } from "../../ui/React/StatsTable";
|
||||
import { MoneyRate } from "../../ui/React/MoneyRate";
|
||||
import { useGang } from "./Context";
|
||||
import { GangMember } from "../GangMember";
|
||||
import { TaskDescription } from "./TaskDescription";
|
||||
|
||||
import { Box } from "@mui/material";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select, { SelectChangeEvent } from "@mui/material/Select";
|
||||
|
||||
import { GangMember } from "../GangMember";
|
||||
|
||||
interface IProps {
|
||||
member: GangMember;
|
||||
onTaskChange: () => void;
|
||||
@@ -29,16 +30,9 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
|
||||
const tasks = gang.getAllTaskNames();
|
||||
|
||||
const data = [
|
||||
[`Money:`, <MoneyRate money={5 * props.member.calculateMoneyGain(gang)} />],
|
||||
[`Respect:`, `${numeralWrapper.formatRespect(5 * props.member.calculateRespectGain(gang))} / sec`],
|
||||
[`Wanted Level:`, `${numeralWrapper.formatWanted(5 * props.member.calculateWantedLevelGain(gang))} / sec`],
|
||||
[`Total Respect:`, `${numeralWrapper.formatRespect(props.member.earnedRespect)}`],
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select onChange={onChange} value={currentTask}>
|
||||
<Box>
|
||||
<Select onChange={onChange} value={currentTask} sx={{ width: '100%' }}>
|
||||
<MenuItem key={0} value={"Unassigned"}>
|
||||
Unassigned
|
||||
</MenuItem>
|
||||
@@ -48,8 +42,7 @@ export function TaskSelector(props: IProps): React.ReactElement {
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<StatsTable rows={data} />
|
||||
</>
|
||||
<TaskDescription member={props.member} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ export function calculateHackingExpGain(server: Server, player: IPlayer): number
|
||||
server.baseDifficulty = server.hackDifficulty;
|
||||
}
|
||||
let expGain = baseExpGain;
|
||||
expGain += server.baseDifficulty * player.hacking_exp_mult * diffFactor;
|
||||
expGain += server.baseDifficulty * diffFactor;
|
||||
|
||||
return expGain * BitNodeMultipliers.HackExpGain;
|
||||
return expGain * player.hacking_exp_mult * BitNodeMultipliers.HackExpGain;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Blackjack } from "../../Casino/Blackjack";
|
||||
import { Blackjack, DECK_COUNT } from "../../Casino/Blackjack";
|
||||
import { CoinFlip } from "../../Casino/CoinFlip";
|
||||
import { Roulette } from "../../Casino/Roulette";
|
||||
import { SlotMachine } from "../../Casino/SlotMachine";
|
||||
@@ -38,7 +38,7 @@ export function CasinoLocation(props: IProps): React.ReactElement {
|
||||
<Button onClick={() => updateGame(GameType.Coin)}>Play coin flip</Button>
|
||||
<Button onClick={() => updateGame(GameType.Slots)}>Play slots</Button>
|
||||
<Button onClick={() => updateGame(GameType.Roulette)}>Play roulette</Button>
|
||||
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack</Button>
|
||||
<Button onClick={() => updateGame(GameType.Blackjack)}>Play blackjack ({DECK_COUNT} decks)</Button>
|
||||
</Box>
|
||||
)}
|
||||
{game !== GameType.None && (
|
||||
|
||||
@@ -114,6 +114,7 @@ export const RamCosts: IMap<any> = {
|
||||
weaken: RamCostConstants.ScriptWeakenRamCost,
|
||||
weakenAnalyze: RamCostConstants.ScriptWeakenAnalyzeRamCost,
|
||||
print: 0,
|
||||
printf: 0,
|
||||
tprint: 0,
|
||||
clearLog: 0,
|
||||
disableLog: 0,
|
||||
|
||||
@@ -553,7 +553,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
if (isNaN(hackAmount)) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
"hackAnalyzeThreads",
|
||||
`Invalid growth argument passed into hackAnalyzeThreads: ${hackAmount}. Must be numeric.`,
|
||||
`Invalid hackAmount argument passed into hackAnalyzeThreads: ${hackAmount}. Must be numeric.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -751,6 +751,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
}
|
||||
workerScript.print(argsToString(args));
|
||||
},
|
||||
printf: function (format: string, ...args: any[]): void {
|
||||
if (typeof format !== "string") {
|
||||
throw makeRuntimeErrorMsg("printf", "First argument must be string for the format.");
|
||||
}
|
||||
workerScript.print(vsprintf(format, args));
|
||||
},
|
||||
tprint: function (...args: any[]): void {
|
||||
if (args.length === 0) {
|
||||
throw makeRuntimeErrorMsg("tprint", "Takes at least 1 argument.");
|
||||
@@ -1676,7 +1682,12 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS {
|
||||
|
||||
const cost = getPurchaseServerCost(ram);
|
||||
if (cost === Infinity) {
|
||||
workerScript.log("purchaseServer", () => `Invalid argument: ram='${ram}' must be a positive power of 2`);
|
||||
if(ram > getPurchaseServerMaxRam()){
|
||||
workerScript.log("purchaseServer", () => `Invalid argument: ram='${ram}' must not be greater than getPurchaseServerMaxRam`);
|
||||
}else{
|
||||
workerScript.log("purchaseServer", () => `Invalid argument: ram='${ram}' must be a positive power of 2`);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
SetMaterialMarketTA2,
|
||||
SetProductMarketTA1,
|
||||
SetProductMarketTA2,
|
||||
SetSmartSupplyUseLeftovers,
|
||||
} from "../Corporation/Actions";
|
||||
import { CorporationUnlockUpgrades } from "../Corporation/data/CorporationUnlockUpgrades";
|
||||
import { CorporationUpgrades } from "../Corporation/data/CorporationUpgrades";
|
||||
@@ -410,6 +411,16 @@ export function NetscriptCorporation(
|
||||
const warehouse = getWarehouse(divisionName, cityName);
|
||||
SetSmartSupply(warehouse, enabled);
|
||||
},
|
||||
setSmartSupplyUseLeftovers: function (adivisionName: any, acityName: any, amaterialName: any, aenabled: any): void {
|
||||
checkAccess("setSmartSupplyUseLeftovers", 7);
|
||||
const divisionName = helper.string("setSmartSupply", "divisionName", adivisionName);
|
||||
const cityName = helper.string("sellProduct", "cityName", acityName);
|
||||
const materialName = helper.string("sellProduct", "materialName", amaterialName);
|
||||
const enabled = helper.boolean(aenabled);
|
||||
const warehouse = getWarehouse(divisionName, cityName);
|
||||
const material = getMaterial(divisionName, cityName, materialName);
|
||||
SetSmartSupplyUseLeftovers(warehouse, material, enabled);
|
||||
},
|
||||
buyMaterial: function (adivisionName: any, acityName: any, amaterialName: any, aamt: any): void {
|
||||
checkAccess("buyMaterial", 7);
|
||||
const divisionName = helper.string("buyMaterial", "divisionName", adivisionName);
|
||||
|
||||
@@ -81,41 +81,41 @@ export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, h
|
||||
return {
|
||||
skills: {
|
||||
calculateSkill: function (exp: any, mult: any = 1): any {
|
||||
checkFormulasAccess("basic.calculateSkill");
|
||||
checkFormulasAccess("skills.calculateSkill");
|
||||
return calculateSkill(exp, mult);
|
||||
},
|
||||
calculateExp: function (skill: any, mult: any = 1): any {
|
||||
checkFormulasAccess("basic.calculateExp");
|
||||
checkFormulasAccess("skills.calculateExp");
|
||||
return calculateExp(skill, mult);
|
||||
},
|
||||
},
|
||||
hacking: {
|
||||
hackChance: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackChance");
|
||||
checkFormulasAccess("hacking.hackChance");
|
||||
return calculateHackingChance(server, player);
|
||||
},
|
||||
hackExp: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackExp");
|
||||
checkFormulasAccess("hacking.hackExp");
|
||||
return calculateHackingExpGain(server, player);
|
||||
},
|
||||
hackPercent: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackPercent");
|
||||
checkFormulasAccess("hacking.hackPercent");
|
||||
return calculatePercentMoneyHacked(server, player);
|
||||
},
|
||||
growPercent: function (server: any, threads: any, player: any, cores: any = 1): any {
|
||||
checkFormulasAccess("basic.growPercent");
|
||||
checkFormulasAccess("hacking.growPercent");
|
||||
return calculateServerGrowth(server, threads, player, cores);
|
||||
},
|
||||
hackTime: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.hackTime");
|
||||
checkFormulasAccess("hacking.hackTime");
|
||||
return calculateHackingTime(server, player) * 1000;
|
||||
},
|
||||
growTime: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.growTime");
|
||||
checkFormulasAccess("hacking.growTime");
|
||||
return calculateGrowTime(server, player) * 1000;
|
||||
},
|
||||
weakenTime: function (server: any, player: any): any {
|
||||
checkFormulasAccess("basic.weakenTime");
|
||||
checkFormulasAccess("hacking.weakenTime");
|
||||
return calculateWeakenTime(server, player) * 1000;
|
||||
},
|
||||
},
|
||||
@@ -188,21 +188,27 @@ export function NetscriptFormulas(player: IPlayer, workerScript: WorkerScript, h
|
||||
},
|
||||
gang: {
|
||||
wantedPenalty(gang: any): number {
|
||||
checkFormulasAccess("gang.wantedPenalty");
|
||||
return calculateWantedPenalty(gang);
|
||||
},
|
||||
respectGain: function (gang: any, member: any, task: any): number {
|
||||
checkFormulasAccess("gang.respectGain");
|
||||
return calculateRespectGain(gang, member, task);
|
||||
},
|
||||
wantedLevelGain: function (gang: any, member: any, task: any): number {
|
||||
checkFormulasAccess("gang.wantedLevelGain");
|
||||
return calculateWantedLevelGain(gang, member, task);
|
||||
},
|
||||
moneyGain: function (gang: any, member: any, task: any): number {
|
||||
checkFormulasAccess("gang.moneyGain");
|
||||
return calculateMoneyGain(gang, member, task);
|
||||
},
|
||||
ascensionPointsGain: function (exp: any): number {
|
||||
checkFormulasAccess("gang.ascensionPointsGain");
|
||||
return calculateAscensionPointsGain(exp);
|
||||
},
|
||||
ascensionMultiplier: function (points: any): number {
|
||||
checkFormulasAccess("gang.ascensionMultiplier");
|
||||
return calculateAscensionMult(points);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -474,7 +474,8 @@ export function NetscriptSingularity(
|
||||
case CityName.Ishima:
|
||||
case CityName.Volhaven:
|
||||
if (player.money < CONSTANTS.TravelCost) {
|
||||
throw helper.makeRuntimeErrorMsg("travelToCity", "Not enough money to travel.");
|
||||
workerScript.log("travelToCity", () => "Not enough money to travel.");
|
||||
return false
|
||||
}
|
||||
player.loseMoney(CONSTANTS.TravelCost, "other");
|
||||
player.city = cityname;
|
||||
@@ -482,8 +483,7 @@ export function NetscriptSingularity(
|
||||
player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
|
||||
return true;
|
||||
default:
|
||||
workerScript.log("travelToCity", () => `Invalid city name: '${cityname}'.`);
|
||||
return false;
|
||||
throw helper.makeRuntimeErrorMsg("travelToCity", `Invalid city name: '${cityname}'.`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { IPlayer } from "../PersonObjects/IPlayer";
|
||||
import { getRamCost } from "../Netscript/RamCostGenerator";
|
||||
import { GameInfo, IStyleSettings, UserInterface as IUserInterface, UserInterfaceTheme } from "../ScriptEditor/NetscriptDefinitions";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { ThemeEvents } from "../ui/React/Theme";
|
||||
import { defaultTheme } from "../Settings/Themes";
|
||||
import { defaultStyles } from "../Settings/Styles";
|
||||
import { ThemeEvents } from "../Themes/ui/Theme";
|
||||
import { defaultTheme } from "../Themes/Themes";
|
||||
import { defaultStyles } from "../Themes/Styles";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { hash } from "../hash/hash";
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface IPlayer {
|
||||
sourceFiles: IPlayerOwnedSourceFile[];
|
||||
exploits: Exploit[];
|
||||
achievements: PlayerAchievement[];
|
||||
terminalCommandHistory: string[];
|
||||
lastUpdate: number;
|
||||
totalPlaytime: number;
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ import { CityName } from "../../Locations/data/CityNames";
|
||||
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
|
||||
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
|
||||
import { ISkillProgress } from "../formulas/skill";
|
||||
import { PlayerAchievement } from '../../Achievements/Achievements';
|
||||
import { PlayerAchievement } from "../../Achievements/Achievements";
|
||||
import { cyrb53 } from "../../utils/StringHelperFunctions";
|
||||
import { getRandomInt } from "../../utils/helpers/getRandomInt";
|
||||
|
||||
export class PlayerObject implements IPlayer {
|
||||
// Class members
|
||||
@@ -77,7 +79,10 @@ export class PlayerObject implements IPlayer {
|
||||
sourceFiles: IPlayerOwnedSourceFile[];
|
||||
exploits: Exploit[];
|
||||
achievements: PlayerAchievement[];
|
||||
terminalCommandHistory: string[];
|
||||
identifier: string;
|
||||
lastUpdate: number;
|
||||
lastSave: number;
|
||||
totalPlaytime: number;
|
||||
|
||||
// Stats
|
||||
@@ -459,7 +464,9 @@ export class PlayerObject implements IPlayer {
|
||||
|
||||
//Used to store the last update time.
|
||||
this.lastUpdate = 0;
|
||||
this.lastSave = 0;
|
||||
this.totalPlaytime = 0;
|
||||
|
||||
this.playtimeSinceLastAug = 0;
|
||||
this.playtimeSinceLastBitnode = 0;
|
||||
|
||||
@@ -471,6 +478,17 @@ export class PlayerObject implements IPlayer {
|
||||
|
||||
this.exploits = [];
|
||||
this.achievements = [];
|
||||
this.terminalCommandHistory = [];
|
||||
|
||||
// Let's get a hash of some semi-random stuff so we have something unique.
|
||||
this.identifier = cyrb53(
|
||||
"I-" +
|
||||
new Date().getTime() +
|
||||
navigator.userAgent +
|
||||
window.innerWidth +
|
||||
window.innerHeight +
|
||||
getRandomInt(100, 999),
|
||||
);
|
||||
|
||||
this.init = generalMethods.init;
|
||||
this.prestigeAugmentation = generalMethods.prestigeAugmentation;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Sleeve } from "../Sleeve";
|
||||
import { numeralWrapper } from "../../../ui/numeralFormat";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
|
||||
import { CONSTANTS } from "../../../Constants";
|
||||
import { Typography } from "@mui/material";
|
||||
import { StatsTable } from "../../../ui/React/StatsTable";
|
||||
import { Modal } from "../../../ui/React/Modal";
|
||||
import React from "react";
|
||||
@@ -80,6 +83,13 @@ export function MoreStatsModal(props: IProps): React.ReactElement {
|
||||
]}
|
||||
title="Multipliers:"
|
||||
/>
|
||||
|
||||
{/* Check for storedCycles to be a bit over 0 to prevent jittering */}
|
||||
{props.sleeve.storedCycles > 10 && (
|
||||
<Typography sx={{ py: 2 }}>
|
||||
Bonus Time: {convertTimeMsToTimeElapsedString(props.sleeve.storedCycles * CONSTANTS.MilliPerCycle)}
|
||||
</Typography>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,11 +22,44 @@ import { v1APIBreak } from "./utils/v1APIBreak";
|
||||
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
|
||||
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
|
||||
import { LocationName } from "./Locations/data/LocationNames";
|
||||
import { SxProps } from "@mui/system";
|
||||
import { PlayerObject } from "./PersonObjects/Player/PlayerObject";
|
||||
import { pushGameSaved } from "./Electron";
|
||||
|
||||
/* SaveObject.js
|
||||
* Defines the object used to save/load games
|
||||
*/
|
||||
|
||||
export interface SaveData {
|
||||
playerIdentifier: string;
|
||||
fileName: string;
|
||||
save: string;
|
||||
savedOn: number;
|
||||
}
|
||||
|
||||
export interface ImportData {
|
||||
base64: string;
|
||||
parsed: any;
|
||||
playerData?: ImportPlayerData;
|
||||
}
|
||||
|
||||
export interface ImportPlayerData {
|
||||
identifier: string;
|
||||
lastSave: number;
|
||||
totalPlaytime: number;
|
||||
|
||||
money: number;
|
||||
hacking: number;
|
||||
|
||||
augmentations: number;
|
||||
factions: number;
|
||||
achievements: number;
|
||||
|
||||
bitNode: number;
|
||||
bitNodeLevel: number;
|
||||
sourceFiles: number;
|
||||
}
|
||||
|
||||
class BitburnerSaveObject {
|
||||
PlayerSave = "";
|
||||
AllServersSave = "";
|
||||
@@ -41,7 +74,6 @@ class BitburnerSaveObject {
|
||||
AllGangsSave = "";
|
||||
LastExportBonus = "";
|
||||
StaneksGiftSave = "";
|
||||
SaveTimestamp = "";
|
||||
|
||||
getSaveString(excludeRunningScripts = false): string {
|
||||
this.PlayerSave = JSON.stringify(Player);
|
||||
@@ -57,7 +89,6 @@ class BitburnerSaveObject {
|
||||
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
||||
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
||||
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
||||
this.SaveTimestamp = new Date().getTime().toString();
|
||||
|
||||
if (Player.inGang()) {
|
||||
this.AllGangsSave = JSON.stringify(AllGangs);
|
||||
@@ -67,28 +98,134 @@ class BitburnerSaveObject {
|
||||
return saveString;
|
||||
}
|
||||
|
||||
saveGame(emitToastEvent = true): void {
|
||||
saveGame(emitToastEvent = true): Promise<void> {
|
||||
const savedOn = new Date().getTime();
|
||||
Player.lastSave = savedOn;
|
||||
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
||||
return new Promise((resolve, reject) => {
|
||||
save(saveString)
|
||||
.then(() => {
|
||||
const saveData: SaveData = {
|
||||
playerIdentifier: Player.identifier,
|
||||
fileName: this.getSaveFileName(),
|
||||
save: saveString,
|
||||
savedOn,
|
||||
};
|
||||
pushGameSaved(saveData);
|
||||
|
||||
save(saveString)
|
||||
.then(() => {
|
||||
if (emitToastEvent) {
|
||||
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
if (emitToastEvent) {
|
||||
SnackbarEvents.emit("Game Saved!", "info", 2000);
|
||||
}
|
||||
return resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return reject();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getSaveFileName(isRecovery = false): string {
|
||||
// Save file name is based on current timestamp and BitNode
|
||||
const epochTime = Math.round(Date.now() / 1000);
|
||||
const bn = Player.bitNodeN;
|
||||
let filename = `bitburnerSave_${epochTime}_BN${bn}x${SourceFileFlags[bn]}.json`;
|
||||
if (isRecovery) filename = "RECOVERY" + filename;
|
||||
return filename;
|
||||
}
|
||||
|
||||
exportGame(): void {
|
||||
const saveString = this.getSaveString(Settings.ExcludeRunningScriptsFromSave);
|
||||
|
||||
// Save file name is based on current timestamp and BitNode
|
||||
const epochTime = Math.round(Date.now() / 1000);
|
||||
const bn = Player.bitNodeN;
|
||||
const filename = `bitburnerSave_${epochTime}_BN${bn}x${SourceFileFlags[bn]}.json`;
|
||||
const filename = this.getSaveFileName();
|
||||
download(filename, saveString);
|
||||
}
|
||||
|
||||
importGame(base64Save: string, reload = true): Promise<void> {
|
||||
if (!base64Save || base64Save === "") throw new Error("Invalid import string");
|
||||
return save(base64Save).then(() => {
|
||||
if (reload) setTimeout(() => location.reload(), 1000);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
getImportStringFromFile(files: FileList | null): Promise<string> {
|
||||
if (files === null) return Promise.reject(new Error("No file selected"));
|
||||
const file = files[0];
|
||||
if (!file) return Promise.reject(new Error("Invalid file selected"));
|
||||
|
||||
const reader = new FileReader();
|
||||
const promise: Promise<string> = new Promise((resolve, reject) => {
|
||||
reader.onload = function (this: FileReader, e: ProgressEvent<FileReader>) {
|
||||
const target = e.target;
|
||||
if (target === null) {
|
||||
return reject(new Error("Error importing file"));
|
||||
}
|
||||
const result = target.result;
|
||||
if (typeof result !== "string" || result === null) {
|
||||
return reject(new Error("FileReader event was not type string"));
|
||||
}
|
||||
const contents = result;
|
||||
resolve(contents);
|
||||
};
|
||||
});
|
||||
reader.readAsText(file);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async getImportDataFromString(base64Save: string): Promise<ImportData> {
|
||||
if (!base64Save || base64Save === "") throw new Error("Invalid import string");
|
||||
|
||||
let newSave;
|
||||
try {
|
||||
newSave = window.atob(base64Save);
|
||||
newSave = newSave.trim();
|
||||
} catch (error) {
|
||||
console.error(error); // We'll handle below
|
||||
}
|
||||
|
||||
if (!newSave || newSave === "") {
|
||||
return Promise.reject(new Error("Save game had not content or was not base64 encoded"));
|
||||
}
|
||||
|
||||
let parsedSave;
|
||||
try {
|
||||
parsedSave = JSON.parse(newSave);
|
||||
} catch (error) {
|
||||
console.log(error); // We'll handle below
|
||||
}
|
||||
|
||||
if (!parsedSave || parsedSave.ctor !== "BitburnerSaveObject" || !parsedSave.data) {
|
||||
return Promise.reject(new Error("Save game did not seem valid"));
|
||||
}
|
||||
|
||||
const data: ImportData = {
|
||||
base64: base64Save,
|
||||
parsed: parsedSave,
|
||||
};
|
||||
|
||||
const importedPlayer = PlayerObject.fromJSON(JSON.parse(parsedSave.data.PlayerSave));
|
||||
|
||||
const playerData: ImportPlayerData = {
|
||||
identifier: importedPlayer.identifier,
|
||||
lastSave: importedPlayer.lastSave,
|
||||
totalPlaytime: importedPlayer.totalPlaytime,
|
||||
|
||||
money: importedPlayer.money,
|
||||
hacking: importedPlayer.hacking,
|
||||
|
||||
augmentations: importedPlayer.augmentations?.reduce<number>((total, current) => (total += current.level), 0) ?? 0,
|
||||
factions: importedPlayer.factions?.length ?? 0,
|
||||
achievements: importedPlayer.achievements?.length ?? 0,
|
||||
|
||||
bitNode: importedPlayer.bitNodeN,
|
||||
bitNodeLevel: importedPlayer.sourceFileLvl(Player.bitNodeN) + 1,
|
||||
sourceFiles: importedPlayer.sourceFiles?.reduce<number>((total, current) => (total += current.lvl), 0) ?? 0,
|
||||
};
|
||||
|
||||
data.playerData = playerData;
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return Generic_toJSON("BitburnerSaveObject", this);
|
||||
}
|
||||
@@ -371,6 +508,18 @@ function createScamUpdateText(): void {
|
||||
}
|
||||
}
|
||||
|
||||
const resets: SxProps = {
|
||||
"& h1, & h2, & h3, & h4, & p, & a, & ul": {
|
||||
margin: 0,
|
||||
color: Settings.theme.primary,
|
||||
whiteSpace: "initial",
|
||||
},
|
||||
"& ul": {
|
||||
paddingLeft: "1.5em",
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
};
|
||||
|
||||
function createNewUpdateText(): void {
|
||||
setTimeout(
|
||||
() =>
|
||||
@@ -379,6 +528,7 @@ function createNewUpdateText(): void {
|
||||
"Please report any bugs/issues through the github repository " +
|
||||
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
|
||||
CONSTANTS.LatestUpdate,
|
||||
resets,
|
||||
),
|
||||
1000,
|
||||
);
|
||||
@@ -391,6 +541,7 @@ function createBetaUpdateText(): void {
|
||||
"Please report any bugs/issues through the github repository (https://github.com/danielyxie/bitburner/issues) " +
|
||||
"or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
|
||||
CONSTANTS.LatestUpdate,
|
||||
resets,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ export interface CrimeStats {
|
||||
/** How much money is given */
|
||||
money: number;
|
||||
/** Name of crime */
|
||||
name: number;
|
||||
name: string;
|
||||
/** Milliseconds it takes to attempt the crime */
|
||||
time: number;
|
||||
/** Description of the crime activity */
|
||||
@@ -3667,6 +3667,7 @@ interface SkillsFormulas {
|
||||
interface HackingFormulas {
|
||||
/**
|
||||
* Calculate hack chance.
|
||||
* (Ex: 0.25 would indicate a 25% chance of success.)
|
||||
* @param server - Server info from {@link NS.getServer | getServer}
|
||||
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
||||
* @returns The calculated hack chance.
|
||||
@@ -3683,6 +3684,7 @@ interface HackingFormulas {
|
||||
hackExp(server: Server, player: Player): number;
|
||||
/**
|
||||
* Calculate hack percent for one thread.
|
||||
* (Ex: 0.25 would steal 25% of the server's current value.)
|
||||
* @remarks
|
||||
* Multiply by thread to get total percent hacked.
|
||||
* @param server - Server info from {@link NS.getServer | getServer}
|
||||
@@ -3691,7 +3693,8 @@ interface HackingFormulas {
|
||||
*/
|
||||
hackPercent(server: Server, player: Player): number;
|
||||
/**
|
||||
* Calculate the percent a server would grow.
|
||||
* Calculate the percent a server would grow to.
|
||||
* (Ex: 3.0 would would grow the server to 300% of its current value.)
|
||||
* @param server - Server info from {@link NS.getServer | getServer}
|
||||
* @param threads - Amount of thread.
|
||||
* @param player - Player info from {@link NS.getPlayer | getPlayer}
|
||||
@@ -4231,13 +4234,11 @@ export interface NS extends Singularity {
|
||||
* ```ts
|
||||
* // NS1:
|
||||
* var earnedMoney = hack("foodnstuff");
|
||||
* earnedMoney = earnedMoney + hack("foodnstuff", { threads: 5 }); // Only use 5 threads to hack
|
||||
* ```
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS2:
|
||||
* let earnedMoney = await ns.hack("foodnstuff");
|
||||
* earnedMoney += await ns.hack("foodnstuff", { threads: 5 }); // Only use 5 threads to hack
|
||||
* ```
|
||||
* @param host - Hostname of the target server to hack.
|
||||
* @param opts - Optional parameters for configuring function behavior.
|
||||
@@ -4265,16 +4266,14 @@ export interface NS extends Singularity {
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS1:
|
||||
* var availableMoney = getServerMoneyAvailable("foodnstuff");
|
||||
* var currentMoney = getServerMoneyAvailable("foodnstuff");
|
||||
* currentMoney = currentMoney * (1 + grow("foodnstuff"));
|
||||
* currentMoney = currentMoney * (1 + grow("foodnstuff", { threads: 5 })); // Only use 5 threads to grow
|
||||
* ```
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS2:
|
||||
* let availableMoney = ns.getServerMoneyAvailable("foodnstuff");
|
||||
* let currentMoney = ns.getServerMoneyAvailable("foodnstuff");
|
||||
* currentMoney *= (1 + await ns.grow("foodnstuff"));
|
||||
* currentMoney *= (1 + await ns.grow("foodnstuff", { threads: 5 })); // Only use 5 threads to grow
|
||||
* ```
|
||||
* @param host - Hostname of the target server to grow.
|
||||
* @param opts - Optional parameters for configuring function behavior.
|
||||
@@ -4300,14 +4299,12 @@ export interface NS extends Singularity {
|
||||
* // NS1:
|
||||
* var currentSecurity = getServerSecurityLevel("foodnstuff");
|
||||
* currentSecurity = currentSecurity - weaken("foodnstuff");
|
||||
* currentSecurity = currentSecurity - weaken("foodnstuff", { threads: 5 }); // Only use 5 threads to weaken
|
||||
* ```
|
||||
* @example
|
||||
* ```ts
|
||||
* // NS2:
|
||||
* let currentSecurity = ns.getServerSecurityLevel("foodnstuff");
|
||||
* currentSecurity -= await ns.weaken("foodnstuff");
|
||||
* currentSecurity -= await ns.weaken("foodnstuff", { threads: 5 }); // Only use 5 threads to weaken
|
||||
* ```
|
||||
* @param host - Hostname of the target server to weaken.
|
||||
* @param opts - Optional parameters for configuring function behavior.
|
||||
@@ -4494,6 +4491,17 @@ export interface NS extends Singularity {
|
||||
*/
|
||||
print(...args: any[]): void;
|
||||
|
||||
/**
|
||||
* Prints a formatted string to the script’s logs.
|
||||
* @remarks
|
||||
* RAM cost: 0 GB
|
||||
*
|
||||
* see: https://github.com/alexei/sprintf.js
|
||||
* @param format - format of the message
|
||||
* @param args - Value(s) to be printed.
|
||||
*/
|
||||
printf(format: string, ...args: any[]): void;
|
||||
|
||||
/**
|
||||
* Prints one or more values or variables to the Terminal.
|
||||
* @remarks
|
||||
@@ -5451,7 +5459,7 @@ export interface NS extends Singularity {
|
||||
* @param filename - Optional. Filename or PID of the script.
|
||||
* @param hostname - Optional. Name of host server the script is running on.
|
||||
* @param args - Arguments to identify the script
|
||||
* @returns info about a running script
|
||||
* @returns The info about the running script if found, and null otherwise.
|
||||
*/
|
||||
getRunningScript(filename?: FilenameOrPID, hostname?: string, ...args: (string | number)[]): RunningScript;
|
||||
|
||||
@@ -6234,14 +6242,14 @@ export interface OfficeAPI {
|
||||
/**
|
||||
* Get the cost to unlock research
|
||||
* @param divisionName - Name of the division
|
||||
* @param cityName - Name of the city
|
||||
* @param researchName - Name of the research
|
||||
* @returns cost
|
||||
*/
|
||||
getResearchCost(divisionName: string, researchName: string): number;
|
||||
/**
|
||||
* Gets if you have unlocked a research
|
||||
* @param divisionName - Name of the division
|
||||
* @param cityName - Name of the city
|
||||
* @param researchName - Name of the research
|
||||
* @returns true is unlocked, false if not
|
||||
*/
|
||||
hasResearched(divisionName: string, researchName: string): boolean;
|
||||
@@ -6310,6 +6318,14 @@ export interface WarehouseAPI {
|
||||
* @param enabled - smart supply enabled
|
||||
*/
|
||||
setSmartSupply(divisionName: string, cityName: string, enabled: boolean): void;
|
||||
/**
|
||||
* Set whether smart supply uses leftovers before buying
|
||||
* @param divisionName - Name of the division
|
||||
* @param cityName - Name of the city
|
||||
* @param materialName - Name of the material
|
||||
* @param enabled - smart supply use leftovers enabled
|
||||
*/
|
||||
setSmartSupplyUseLeftovers(divisionName: string, cityName: string, materialName: string, enabled: boolean): void;
|
||||
/**
|
||||
* Set material buy data
|
||||
* @param divisionName - Name of the division
|
||||
|
||||
@@ -33,6 +33,8 @@ import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
import Box from "@mui/material/Box";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
@@ -41,6 +43,7 @@ import { PromptEvent } from "../../ui/React/PromptManager";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
|
||||
import libSource from "!!raw-loader!../NetscriptDefinitions.d.ts";
|
||||
import { Tooltip } from "@mui/material";
|
||||
|
||||
interface IProps {
|
||||
// Map of filename -> code
|
||||
@@ -696,17 +699,58 @@ export function Root(props: IProps): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
function onTabUpdate(index: number): void {
|
||||
const openScript = openScripts[index];
|
||||
const serverScriptCode = getServerCode(index);
|
||||
if (serverScriptCode === null) return;
|
||||
|
||||
if (openScript.code !== serverScriptCode) {
|
||||
PromptEvent.emit({
|
||||
txt:
|
||||
"Do you want to overwrite the current editor content with the contents of " +
|
||||
openScript.fileName +
|
||||
" on the server? This cannot be undone.",
|
||||
resolve: (result: boolean) => {
|
||||
if (result) {
|
||||
// Save changes
|
||||
openScript.code = serverScriptCode;
|
||||
|
||||
// Switch to target tab
|
||||
onTabClick(index);
|
||||
|
||||
if (editorRef.current !== null && openScript !== null) {
|
||||
if (openScript.model === undefined || openScript.model.isDisposed()) {
|
||||
regenerateModel(openScript);
|
||||
}
|
||||
editorRef.current.setModel(openScript.model);
|
||||
|
||||
editorRef.current.setValue(openScript.code);
|
||||
updateRAM(openScript.code);
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function dirty(index: number): string {
|
||||
const openScript = openScripts[index];
|
||||
const serverScriptCode = getServerCode(index);
|
||||
if (serverScriptCode === null) return " *";
|
||||
|
||||
// The server code is stored with its starting & trailing whitespace removed
|
||||
const openScriptFormatted = Script.formatCode(openScript.code);
|
||||
return serverScriptCode !== openScriptFormatted ? " *" : "";
|
||||
}
|
||||
|
||||
function getServerCode(index: number): string | null {
|
||||
const openScript = openScripts[index];
|
||||
const server = GetServer(openScript.hostname);
|
||||
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
|
||||
|
||||
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName);
|
||||
if (serverScript === undefined) return " *";
|
||||
|
||||
// The server code is stored with its starting & trailing whitespace removed
|
||||
const openScriptFormatted = Script.formatCode(openScript.code);
|
||||
return serverScript.code !== openScriptFormatted ? " *" : "";
|
||||
return serverScript?.code ?? null;
|
||||
}
|
||||
|
||||
// Toolbars are roughly 112px:
|
||||
@@ -738,62 +782,78 @@ export function Root(props: IProps): React.ReactElement {
|
||||
overflowX: "scroll",
|
||||
}}
|
||||
>
|
||||
{openScripts.map(({ fileName, hostname }, index) => (
|
||||
<Draggable
|
||||
key={fileName + hostname}
|
||||
draggableId={fileName + hostname}
|
||||
index={index}
|
||||
disableInteractiveElementBlocking={true}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
marginRight: "5px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => onTabClick(index)}
|
||||
style={
|
||||
currentScript?.fileName === openScripts[index].fileName
|
||||
? {
|
||||
background: Settings.theme.button,
|
||||
color: Settings.theme.primary,
|
||||
}
|
||||
: {
|
||||
background: Settings.theme.backgroundsecondary,
|
||||
color: Settings.theme.secondary,
|
||||
}
|
||||
}
|
||||
>
|
||||
{hostname}:~/{fileName} {dirty(index)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onTabClose(index)}
|
||||
{openScripts.map(({ fileName, hostname }, index) => {
|
||||
const iconButtonStyle = {
|
||||
maxWidth: "25px",
|
||||
minWidth: "25px",
|
||||
minHeight: "38.5px",
|
||||
maxHeight: "38.5px",
|
||||
...(currentScript?.fileName === openScripts[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,
|
||||
}),
|
||||
};
|
||||
return (
|
||||
<Draggable
|
||||
key={fileName + hostname}
|
||||
draggableId={fileName + hostname}
|
||||
index={index}
|
||||
disableInteractiveElementBlocking={true}
|
||||
>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
maxWidth: "20px",
|
||||
minWidth: "20px",
|
||||
...(currentScript?.fileName === openScripts[index].fileName
|
||||
? {
|
||||
background: Settings.theme.button,
|
||||
color: Settings.theme.primary,
|
||||
}
|
||||
: {
|
||||
background: Settings.theme.backgroundsecondary,
|
||||
color: Settings.theme.secondary,
|
||||
}),
|
||||
...provided.draggableProps.style,
|
||||
marginRight: "5px",
|
||||
flexShrink: 0,
|
||||
border: "1px solid " + Settings.theme.well,
|
||||
}}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => onTabClick(index)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
if (e.button === 1) onTabClose(index);
|
||||
}}
|
||||
style={{
|
||||
...(currentScript?.fileName === openScripts[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,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{hostname}:~/{fileName} {dirty(index)}
|
||||
</Button>
|
||||
<Tooltip title="Overwrite editor content with saved file content">
|
||||
<Button onClick={() => onTabUpdate(index)} style={iconButtonStyle}>
|
||||
<SyncIcon fontSize="small" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button onClick={() => onTabClose(index)} style={iconButtonStyle}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function getPurchaseServerCost(ram: number): number {
|
||||
|
||||
const upg = Math.max(0, Math.log(sanitizedRam) / Math.log(2) - 6);
|
||||
|
||||
return (
|
||||
return Math.round(
|
||||
sanitizedRam *
|
||||
CONSTANTS.BaseCostFor1GBOfRamServer *
|
||||
BitNodeMultipliers.PurchasedServerCost *
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ISelfInitializer, ISelfLoading } from "../types";
|
||||
import { OwnedAugmentationsOrderSetting, PurchaseAugmentationsOrderSetting } from "./SettingEnums";
|
||||
import { defaultTheme, ITheme } from "./Themes";
|
||||
import { defaultStyles } from "./Styles";
|
||||
import { defaultTheme, ITheme } from "../Themes/Themes";
|
||||
import { defaultStyles } from "../Themes/Styles";
|
||||
import { WordWrapOptions } from "../ScriptEditor/ui/Options";
|
||||
import { OverviewSettings } from "../ui/React/Overview";
|
||||
import { IStyleSettings } from "../ScriptEditor/NetscriptDefinitions";
|
||||
|
||||
@@ -1,613 +0,0 @@
|
||||
import { IMap } from "../types";
|
||||
|
||||
export interface ITheme {
|
||||
[key: string]: string | undefined;
|
||||
primarylight: string;
|
||||
primary: string;
|
||||
primarydark: string;
|
||||
successlight: string;
|
||||
success: string;
|
||||
successdark: string;
|
||||
errorlight: string;
|
||||
error: string;
|
||||
errordark: string;
|
||||
secondarylight: string;
|
||||
secondary: string;
|
||||
secondarydark: string;
|
||||
warninglight: string;
|
||||
warning: string;
|
||||
warningdark: string;
|
||||
infolight: string;
|
||||
info: string;
|
||||
infodark: string;
|
||||
welllight: string;
|
||||
well: string;
|
||||
white: string;
|
||||
black: string;
|
||||
hp: string;
|
||||
money: string;
|
||||
hack: string;
|
||||
combat: string;
|
||||
cha: string;
|
||||
int: string;
|
||||
rep: string;
|
||||
disabled: string;
|
||||
backgroundprimary: string;
|
||||
backgroundsecondary: string;
|
||||
button: string;
|
||||
}
|
||||
|
||||
export interface IPredefinedTheme {
|
||||
colors: ITheme;
|
||||
name?: string;
|
||||
credit?: string;
|
||||
description?: string;
|
||||
reference?: string;
|
||||
}
|
||||
|
||||
export const defaultTheme: ITheme = {
|
||||
primarylight: "#0f0",
|
||||
primary: "#0c0",
|
||||
primarydark: "#090",
|
||||
successlight: "#0f0",
|
||||
success: "#0c0",
|
||||
successdark: "#090",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
};
|
||||
|
||||
export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
|
||||
Default: {
|
||||
colors: defaultTheme,
|
||||
},
|
||||
Monokai: {
|
||||
description: "Monokai'ish",
|
||||
colors: {
|
||||
primarylight: "#FFF",
|
||||
primary: "#F8F8F2",
|
||||
primarydark: "#FAFAEB",
|
||||
successlight: "#ADE146",
|
||||
success: "#A6E22E",
|
||||
successdark: "#98E104",
|
||||
errorlight: "#FF69A0",
|
||||
error: "#F92672",
|
||||
errordark: "#D10F56",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E1D992",
|
||||
warning: "#E6DB74",
|
||||
warningdark: "#EDDD54",
|
||||
infolight: "#92E1F1",
|
||||
info: "#66D9EF",
|
||||
infodark: "#31CDED",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#F92672",
|
||||
money: "#E6DB74",
|
||||
hack: "#A6E22E",
|
||||
combat: "#75715E",
|
||||
cha: "#AE81FF",
|
||||
int: "#66D9EF",
|
||||
rep: "#E69F66",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#272822",
|
||||
backgroundsecondary: "#1B1C18",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
Warmer: {
|
||||
credit: "hexnaught",
|
||||
description: "Warmer, softer theme",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999581020028938",
|
||||
colors: {
|
||||
primarylight: "#EA9062",
|
||||
primary: "#DD7B4A",
|
||||
primarydark: "#D3591C",
|
||||
successlight: "#6ACF6A",
|
||||
success: "#43BF43",
|
||||
successdark: "#3E913E",
|
||||
errorlight: "#C15757",
|
||||
error: "#B34141",
|
||||
errordark: "#752525",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E6E69D",
|
||||
warning: "#DADA56",
|
||||
warningdark: "#A1A106",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#AD84CF",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#76C6B7",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"Dark+": {
|
||||
credit: "LoganMD",
|
||||
description: "VSCode Dark+",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999975867617310",
|
||||
colors: {
|
||||
primarylight: "#E0E0BC",
|
||||
primary: "#CCCCAE",
|
||||
primarydark: "#B8B89C",
|
||||
successlight: "#00F000",
|
||||
success: "#00D200",
|
||||
successdark: "#00B400",
|
||||
errorlight: "#F00000",
|
||||
error: "#C80000",
|
||||
errordark: "#A00000",
|
||||
secondarylight: "#B4AEAE",
|
||||
secondary: "#969090",
|
||||
secondarydark: "#787272",
|
||||
warninglight: "#F0F000",
|
||||
warning: "#C8C800",
|
||||
warningdark: "#A0A000",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#1E1E1E",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#1E1E1E",
|
||||
backgroundsecondary: "#252525",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"Mayukai Dark": {
|
||||
credit: "Festive Noire",
|
||||
description: "Mayukai Dark-esque",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922037502334889994",
|
||||
colors: {
|
||||
primarylight: "#DDDFC5",
|
||||
primary: "#CDCFB6",
|
||||
primarydark: "#9D9F8C",
|
||||
successlight: "#00EF00",
|
||||
success: "#00A500",
|
||||
successdark: "#007A00",
|
||||
errorlight: "#F92672",
|
||||
error: "#CA1C5C",
|
||||
errordark: "#90274A",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#D3D300",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#00010A",
|
||||
white: "#fff",
|
||||
black: "#020509",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#8CCF27",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#080C11",
|
||||
backgroundsecondary: "#03080F",
|
||||
button: "#00010A",
|
||||
},
|
||||
},
|
||||
|
||||
Purple: {
|
||||
credit: "zer0ney",
|
||||
description: "Essentially all defaults except for purple replacing the main colors",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922091815849570395",
|
||||
colors: {
|
||||
primarylight: "#BA55D3",
|
||||
primary: "#9370DB",
|
||||
primarydark: "#8A2BE2",
|
||||
successlight: "#BA55D3",
|
||||
success: "#9370DB",
|
||||
successdark: "#8A2BE2",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"Smooth Green": {
|
||||
credit: "Swidt",
|
||||
description: "A nice green theme that doesn't hurt your eyes.",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922243957986033725",
|
||||
colors: {
|
||||
primarylight: "#E0E0BC",
|
||||
primary: "#B0D9A3",
|
||||
primarydark: "#B8B89C",
|
||||
successlight: "#00F000",
|
||||
success: "#6BC16B",
|
||||
successdark: "#00B400",
|
||||
errorlight: "#F00000",
|
||||
error: "#3D713D",
|
||||
errordark: "#A00000",
|
||||
secondarylight: "#B4AEAE",
|
||||
secondary: "#8FAF85",
|
||||
secondarydark: "#787272",
|
||||
warninglight: "#F0F000",
|
||||
warning: "#38F100",
|
||||
warningdark: "#A0A000",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#2F3C2B",
|
||||
white: "#fff",
|
||||
black: "#1E1E1E",
|
||||
hp: "#dd3434",
|
||||
money: "#4AA52E",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#35A135",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#1E1E1E",
|
||||
backgroundsecondary: "#252525",
|
||||
button: "#2F3C2B",
|
||||
},
|
||||
},
|
||||
|
||||
Dracula: {
|
||||
credit: "H3draut3r",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922296307836678144",
|
||||
colors: {
|
||||
primarylight: "#7082B8",
|
||||
primary: "#F8F8F2",
|
||||
primarydark: "#FF79C6",
|
||||
successlight: "#0f0",
|
||||
success: "#0c0",
|
||||
successdark: "#090",
|
||||
errorlight: "#FD4545",
|
||||
error: "#FF2D2D",
|
||||
errordark: "#C62424",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#8BE9FD",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#FFC281",
|
||||
warning: "#FFB86C",
|
||||
warningdark: "#E6A055",
|
||||
infolight: "#A0A0FF",
|
||||
info: "#7070FF",
|
||||
infodark: "#4040FF",
|
||||
welllight: "#44475A",
|
||||
well: "#363948",
|
||||
white: "#fff",
|
||||
black: "#282A36",
|
||||
hp: "#D34448",
|
||||
money: "#50FA7B",
|
||||
hack: "#F1FA8C",
|
||||
combat: "#BD93F9",
|
||||
cha: "#FF79C6",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#282A36",
|
||||
backgroundsecondary: "#21222C",
|
||||
button: "#21222C",
|
||||
},
|
||||
},
|
||||
|
||||
"Dark Blue": {
|
||||
credit: "Saynt_Garmo",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/923084732718264340",
|
||||
colors: {
|
||||
primarylight: "#023DDE",
|
||||
primary: "#4A41C8",
|
||||
primarydark: "#005299",
|
||||
successlight: "#00FF00",
|
||||
success: "#D1DAD1",
|
||||
successdark: "#BFCABF",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#040505",
|
||||
white: "#fff",
|
||||
black: "#000000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#091419",
|
||||
backgroundsecondary: "#000000",
|
||||
button: "#000000",
|
||||
},
|
||||
},
|
||||
|
||||
Discord: {
|
||||
credit: "Thermite",
|
||||
description: "Discord inspired theme",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924305252017143818",
|
||||
colors: {
|
||||
primarylight: "#7389DC",
|
||||
primary: "#7389DC",
|
||||
primarydark: "#5964F1",
|
||||
successlight: "#00CC00",
|
||||
success: "#20DF20",
|
||||
successdark: "#0CB80C",
|
||||
errorlight: "#EA5558",
|
||||
error: "#EC4145",
|
||||
errordark: "#E82528",
|
||||
secondarylight: "#C3C3C3",
|
||||
secondary: "#9C9C9C",
|
||||
secondarydark: "#4E4E4E",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#1C4FB3",
|
||||
welllight: "#999999",
|
||||
well: "#35383C",
|
||||
white: "#FFFFFF",
|
||||
black: "#202225",
|
||||
hp: "#FF5656",
|
||||
money: "#43FF43",
|
||||
hack: "#FFAB3D",
|
||||
combat: "#8A90FD",
|
||||
cha: "#FF51D9",
|
||||
int: "#6495ed",
|
||||
rep: "#FFFF30",
|
||||
disabled: "#474B51",
|
||||
backgroundprimary: "#2F3136",
|
||||
backgroundsecondary: "#35393E",
|
||||
button: "#333",
|
||||
},
|
||||
},
|
||||
|
||||
"One Dark": {
|
||||
credit: "Dexalt142",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924650660694208512",
|
||||
colors: {
|
||||
primarylight: "#98C379",
|
||||
primary: "#98C379",
|
||||
primarydark: "#98C379",
|
||||
successlight: "#98C379",
|
||||
success: "#98C379",
|
||||
successdark: "#98C379",
|
||||
errorlight: "#E06C75",
|
||||
error: "#BE5046",
|
||||
errordark: "#BE5046",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E5C07B",
|
||||
warning: "#E5C07B",
|
||||
warningdark: "#D19A66",
|
||||
infolight: "#61AFEF",
|
||||
info: "#61AFEF",
|
||||
infodark: "#61AFEF",
|
||||
welllight: "#4B5263",
|
||||
well: "#282C34",
|
||||
white: "#ABB2BF",
|
||||
black: "#282C34",
|
||||
hp: "#E06C75",
|
||||
money: "#E5C07B",
|
||||
hack: "#98C379",
|
||||
combat: "#ABB2BF",
|
||||
cha: "#C678DD",
|
||||
int: "#61AFEF",
|
||||
rep: "#ABB2BF",
|
||||
disabled: "#56B6C2",
|
||||
backgroundprimary: "#282C34",
|
||||
backgroundsecondary: "#21252B",
|
||||
button: "#4B5263",
|
||||
},
|
||||
},
|
||||
|
||||
"Muted Gold & Blue": {
|
||||
credit: "Sloth",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924672660758208563",
|
||||
colors: {
|
||||
primarylight: "#E3B54A",
|
||||
primary: "#CAA243",
|
||||
primarydark: "#7E6937",
|
||||
successlight: "#82FF82",
|
||||
success: "#6FDA6F",
|
||||
successdark: "#64C364",
|
||||
errorlight: "#FD5555",
|
||||
error: "#D84A4A",
|
||||
errordark: "#AC3939",
|
||||
secondarylight: "#D8D0B8",
|
||||
secondary: "#B1AA95",
|
||||
secondarydark: "#736E5E",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#111111",
|
||||
white: "#fff",
|
||||
black: "#070300",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#0A0A0E",
|
||||
backgroundsecondary: "#0E0E10",
|
||||
button: "#222222",
|
||||
},
|
||||
},
|
||||
|
||||
"Default Lite": {
|
||||
credit: "NmuGmu",
|
||||
description: "Less eye-straining default theme",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
|
||||
colors: {
|
||||
primarylight: "#28CF28",
|
||||
primary: "#21A821",
|
||||
primarydark: "#177317",
|
||||
successlight: "#1CFF1C",
|
||||
success: "#16CA16",
|
||||
successdark: "#0D910D",
|
||||
errorlight: "#FF3B3B",
|
||||
error: "#C32D2D",
|
||||
errordark: "#8E2121",
|
||||
secondarylight: "#B3B3B3",
|
||||
secondary: "#838383",
|
||||
secondarydark: "#676767",
|
||||
warninglight: "#FFFF3A",
|
||||
warning: "#C3C32A",
|
||||
warningdark: "#8C8C1E",
|
||||
infolight: "#64CBFF",
|
||||
info: "#3399CC",
|
||||
infodark: "#246D91",
|
||||
welllight: "#404040",
|
||||
well: "#1C1C1C",
|
||||
white: "#C3C3C3",
|
||||
black: "#0A0B0B",
|
||||
hp: "#C62E2E",
|
||||
money: "#D6BB27",
|
||||
hack: "#ADFF2F",
|
||||
combat: "#E8EDCD",
|
||||
cha: "#8B5FAF",
|
||||
int: "#537CC8",
|
||||
rep: "#E8EDCD",
|
||||
disabled: "#5AB5A5",
|
||||
backgroundprimary: "#0C0D0E",
|
||||
backgroundsecondary: "#121415",
|
||||
button: "#252829",
|
||||
},
|
||||
},
|
||||
|
||||
Light: {
|
||||
credit: "matt",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/926114005456658432",
|
||||
colors: {
|
||||
primarylight: "#535353",
|
||||
primary: "#1A1A1A",
|
||||
primarydark: "#0d0d0d",
|
||||
successlight: "#63c439",
|
||||
success: "#428226",
|
||||
successdark: "#2E5A1B",
|
||||
errorlight: "#df7051",
|
||||
error: "#C94824",
|
||||
errordark: "#91341B",
|
||||
secondarylight: "#b3b3b3",
|
||||
secondary: "#9B9B9B",
|
||||
secondarydark: "#7A7979",
|
||||
warninglight: "#e8d464",
|
||||
warning: "#C6AD20",
|
||||
warningdark: "#9F8A16",
|
||||
infolight: "#6299cf",
|
||||
info: "#3778B7",
|
||||
infodark: "#30689C",
|
||||
welllight: "#f9f9f9",
|
||||
well: "#eaeaea",
|
||||
white: "#F7F7F7",
|
||||
black: "#F7F7F7",
|
||||
hp: "#BF5C41",
|
||||
money: "#E1B121",
|
||||
hack: "#47BC38",
|
||||
combat: "#656262",
|
||||
cha: "#A568AC",
|
||||
int: "#889BCF",
|
||||
rep: "#656262",
|
||||
disabled: "#70B4BF",
|
||||
backgroundprimary: "#F7F7F7",
|
||||
backgroundsecondary: "#f9f9f9",
|
||||
button: "#eaeaea",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export const TerminalHelpText: string[] = [
|
||||
" grow Spoof money in a servers bank account, increasing the amount available.",
|
||||
" hack Hack the current machine",
|
||||
" help [command] Display this help text, or the help text for a command",
|
||||
" history [-c] Display the terminal history",
|
||||
" home Connect to home computer",
|
||||
" hostname Displays the hostname of the machine",
|
||||
" kill [script/pid] [args...] Stops the specified script on the current server ",
|
||||
@@ -255,6 +256,12 @@ export const HelpTexts: IMap<string[]> = {
|
||||
" help scan-analyze",
|
||||
" ",
|
||||
],
|
||||
history: [
|
||||
"Usage: history [-c]",
|
||||
" ",
|
||||
"Without arguments, displays the terminal command history. To clear the history, pass in the '-c' argument.",
|
||||
" ",
|
||||
],
|
||||
home: [
|
||||
"Usage: home", " ", "Connect to your home computer. This will work no matter what server you are currently connected to.", " ",
|
||||
],
|
||||
|
||||
@@ -48,6 +48,7 @@ import { free } from "./commands/free";
|
||||
import { grow } from "./commands/grow";
|
||||
import { hack } from "./commands/hack";
|
||||
import { help } from "./commands/help";
|
||||
import { history } from "./commands/history";
|
||||
import { home } from "./commands/home";
|
||||
import { hostname } from "./commands/hostname";
|
||||
import { kill } from "./commands/kill";
|
||||
@@ -143,7 +144,7 @@ export class Terminal implements ITerminal {
|
||||
startGrow(player: IPlayer): void {
|
||||
const server = player.getCurrentServer();
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot grow this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@@ -152,7 +153,7 @@ export class Terminal implements ITerminal {
|
||||
startWeaken(player: IPlayer): void {
|
||||
const server = player.getCurrentServer();
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot weaken this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@@ -241,7 +242,7 @@ export class Terminal implements ITerminal {
|
||||
if (cancelled) return;
|
||||
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot grow this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@@ -268,7 +269,7 @@ export class Terminal implements ITerminal {
|
||||
if (cancelled) return;
|
||||
|
||||
if (server instanceof HacknetServer) {
|
||||
this.error("Cannot hack this kind of server");
|
||||
this.error("Cannot weaken this kind of server");
|
||||
return;
|
||||
}
|
||||
if (!(server instanceof Server)) throw new Error("server should be normal server");
|
||||
@@ -576,6 +577,7 @@ export class Terminal implements ITerminal {
|
||||
if (this.commandHistory.length > 50) {
|
||||
this.commandHistory.splice(0, 1);
|
||||
}
|
||||
player.terminalCommandHistory = this.commandHistory;
|
||||
}
|
||||
this.commandHistoryIndex = this.commandHistory.length;
|
||||
const allCommands = ParseCommands(commands);
|
||||
@@ -785,6 +787,7 @@ export class Terminal implements ITerminal {
|
||||
grow: grow,
|
||||
hack: hack,
|
||||
help: help,
|
||||
history: history,
|
||||
home: home,
|
||||
hostname: hostname,
|
||||
kill: kill,
|
||||
|
||||
@@ -6,6 +6,37 @@ import { isScriptFilename } from "../../Script/isScriptFilename";
|
||||
import FileSaver from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
|
||||
export function exportScripts(pattern: string, server: BaseServer): void {
|
||||
const matchEnding = pattern.length == 1 || pattern === "*.*" ? null : pattern.slice(1); // Treat *.* the same as *
|
||||
const zip = new JSZip();
|
||||
// Helper function to zip any file contents whose name matches the pattern
|
||||
const zipFiles = (fileNames: string[], fileContents: string[]): void => {
|
||||
for (let i = 0; i < fileContents.length; ++i) {
|
||||
let name = fileNames[i];
|
||||
if (name.startsWith("/")) name = name.slice(1);
|
||||
if (!matchEnding || name.endsWith(matchEnding))
|
||||
zip.file(name, new Blob([fileContents[i]], { type: "text/plain" }));
|
||||
}
|
||||
};
|
||||
// In the case of script files, we pull from the server.scripts array
|
||||
if (!matchEnding || isScriptFilename(matchEnding))
|
||||
zipFiles(
|
||||
server.scripts.map((s) => s.filename),
|
||||
server.scripts.map((s) => s.code),
|
||||
);
|
||||
// In the case of text files, we pull from the server.scripts array
|
||||
if (!matchEnding || matchEnding.endsWith(".txt"))
|
||||
zipFiles(
|
||||
server.textFiles.map((s) => s.fn),
|
||||
server.textFiles.map((s) => s.text),
|
||||
);
|
||||
|
||||
// Return an error if no files matched, rather than an empty zip folder
|
||||
if (Object.keys(zip.files).length == 0) throw new Error(`No files match the pattern ${pattern}`);
|
||||
const zipFn = `bitburner${isScriptFilename(pattern) ? "Scripts" : pattern === "*.txt" ? "Texts" : "Files"}.zip`;
|
||||
zip.generateAsync({ type: "blob" }).then((content: any) => FileSaver.saveAs(content, zipFn));
|
||||
}
|
||||
|
||||
export function download(
|
||||
terminal: ITerminal,
|
||||
router: IRouter,
|
||||
@@ -21,34 +52,12 @@ export function download(
|
||||
const fn = args[0] + "";
|
||||
// If the parameter starts with *, download all files that match the wildcard pattern
|
||||
if (fn.startsWith("*")) {
|
||||
const matchEnding = fn.length == 1 || fn === "*.*" ? null : fn.slice(1); // Treat *.* the same as *
|
||||
const zip = new JSZip();
|
||||
// Helper function to zip any file contents whose name matches the pattern
|
||||
const zipFiles = (fileNames: string[], fileContents: string[]): void => {
|
||||
for (let i = 0; i < fileContents.length; ++i) {
|
||||
let name = fileNames[i];
|
||||
if (name.startsWith("/")) name = name.slice(1);
|
||||
if (!matchEnding || name.endsWith(matchEnding))
|
||||
zip.file(name, new Blob([fileContents[i]], { type: "text/plain" }));
|
||||
}
|
||||
};
|
||||
// In the case of script files, we pull from the server.scripts array
|
||||
if (!matchEnding || isScriptFilename(matchEnding))
|
||||
zipFiles(
|
||||
server.scripts.map((s) => s.filename),
|
||||
server.scripts.map((s) => s.code),
|
||||
);
|
||||
// In the case of text files, we pull from the server.scripts array
|
||||
if (!matchEnding || matchEnding.endsWith(".txt"))
|
||||
zipFiles(
|
||||
server.textFiles.map((s) => s.fn),
|
||||
server.textFiles.map((s) => s.text),
|
||||
);
|
||||
// Return an error if no files matched, rather than an empty zip folder
|
||||
if (Object.keys(zip.files).length == 0) return terminal.error(`No files match the pattern ${fn}`);
|
||||
const zipFn = `bitburner${isScriptFilename(fn) ? "Scripts" : fn === "*.txt" ? "Texts" : "Files"}.zip`;
|
||||
zip.generateAsync({ type: "blob" }).then((content: any) => FileSaver.saveAs(content, zipFn));
|
||||
return;
|
||||
try {
|
||||
exportScripts(fn, server);
|
||||
return;
|
||||
} catch (error: any) {
|
||||
return terminal.error(error.message);
|
||||
}
|
||||
} else if (isScriptFilename(fn)) {
|
||||
// Download a single script
|
||||
const script = terminal.getScript(player, fn);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ITerminal } from "../ITerminal";
|
||||
import { IRouter } from "../../ui/Router";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
|
||||
export function history(
|
||||
terminal: ITerminal,
|
||||
router: IRouter,
|
||||
player: IPlayer,
|
||||
server: BaseServer,
|
||||
args: (string | number | boolean)[],
|
||||
): void {
|
||||
if (args.length === 0) {
|
||||
terminal.commandHistory.forEach((command, index) => {
|
||||
terminal.print(`${index.toString().padStart(2)} ${command}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const arg = args[0] + "";
|
||||
if (arg === "-c" || arg === "--clear") {
|
||||
player.terminalCommandHistory = [];
|
||||
terminal.commandHistory = [];
|
||||
terminal.commandHistoryIndex = 1;
|
||||
} else {
|
||||
terminal.error("Incorrect usage of history command. usage: history [-c]");
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,12 @@ export function TerminalInput({ terminal, router, player }: IProps): React.React
|
||||
const [possibilities, setPossibilities] = useState<string[]>([]);
|
||||
const classes = useStyles();
|
||||
|
||||
// If we have no data in the current terminal history, let's initialize it from the player save
|
||||
if (terminal.commandHistory.length === 0 && player.terminalCommandHistory.length > 0) {
|
||||
terminal.commandHistory = player.terminalCommandHistory;
|
||||
terminal.commandHistoryIndex = terminal.commandHistory.length;
|
||||
}
|
||||
|
||||
// Need to run after state updates, for example if we need to move cursor
|
||||
// *after* we modify input
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Themes
|
||||
|
||||
Feel free to contribute a new theme by submitting a pull request to the game!
|
||||
|
||||
See [CONTRIBUTING.md](/doc/CONTRIBUTING.md) for details.
|
||||
|
||||
## How create a new theme
|
||||
|
||||
1. Duplicate one of the folders in `/src/Themes/data` and give it a new name (keep the hyphenated format)
|
||||
2. Modify the data in the new `/src/Themes/data/new-folder/index.ts` file
|
||||
3. Replace the screenshot.png with one of your theme
|
||||
4. Add the import/export into the `/src/Themes/data/index.ts` file
|
||||
|
||||
The themes are ordered according to the export order in `index.ts`
|
||||
|
||||
## Other resources
|
||||
|
||||
There is an external script called `theme-browser` which may include more themes than those shown here. Head over the [bitpacker](https://github.com/davidsiems/bitpacker) repository for details.
|
||||
@@ -0,0 +1,56 @@
|
||||
import { IMap } from "../types";
|
||||
import * as predefined from "./data";
|
||||
|
||||
export interface ITheme {
|
||||
[key: string]: string | undefined;
|
||||
primarylight: string;
|
||||
primary: string;
|
||||
primarydark: string;
|
||||
successlight: string;
|
||||
success: string;
|
||||
successdark: string;
|
||||
errorlight: string;
|
||||
error: string;
|
||||
errordark: string;
|
||||
secondarylight: string;
|
||||
secondary: string;
|
||||
secondarydark: string;
|
||||
warninglight: string;
|
||||
warning: string;
|
||||
warningdark: string;
|
||||
infolight: string;
|
||||
info: string;
|
||||
infodark: string;
|
||||
welllight: string;
|
||||
well: string;
|
||||
white: string;
|
||||
black: string;
|
||||
hp: string;
|
||||
money: string;
|
||||
hack: string;
|
||||
combat: string;
|
||||
cha: string;
|
||||
int: string;
|
||||
rep: string;
|
||||
disabled: string;
|
||||
backgroundprimary: string;
|
||||
backgroundsecondary: string;
|
||||
button: string;
|
||||
}
|
||||
|
||||
export interface IPredefinedTheme {
|
||||
colors: ITheme;
|
||||
name: string;
|
||||
credit: string;
|
||||
screenshot: string;
|
||||
description: string;
|
||||
reference?: string;
|
||||
}
|
||||
|
||||
export const defaultTheme: ITheme = {
|
||||
...predefined.Default.colors,
|
||||
};
|
||||
|
||||
export const getPredefinedThemes = (): IMap<IPredefinedTheme> => ({
|
||||
...predefined,
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Dark Blue",
|
||||
description: "Very dark with a blue/purplelly primary",
|
||||
credit: "Saynt_Garmo",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/923084732718264340",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#023DDE",
|
||||
primary: "#4A41C8",
|
||||
primarydark: "#005299",
|
||||
successlight: "#00FF00",
|
||||
success: "#D1DAD1",
|
||||
successdark: "#BFCABF",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#040505",
|
||||
white: "#fff",
|
||||
black: "#000000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#091419",
|
||||
backgroundsecondary: "#000000",
|
||||
button: "#000000",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Dark+",
|
||||
credit: "LoganMD",
|
||||
description: "VSCode Dark+",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999975867617310",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#E0E0BC",
|
||||
primary: "#CCCCAE",
|
||||
primarydark: "#B8B89C",
|
||||
successlight: "#00F000",
|
||||
success: "#00D200",
|
||||
successdark: "#00B400",
|
||||
errorlight: "#F00000",
|
||||
error: "#C80000",
|
||||
errordark: "#A00000",
|
||||
secondarylight: "#B4AEAE",
|
||||
secondary: "#969090",
|
||||
secondarydark: "#787272",
|
||||
warninglight: "#F0F000",
|
||||
warning: "#C8C800",
|
||||
warningdark: "#A0A000",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#1E1E1E",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#1E1E1E",
|
||||
backgroundsecondary: "#252525",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 117 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Default-lite",
|
||||
description: "Less eye-straining default theme",
|
||||
credit: "NmuGmu",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/925263801564151888",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#28CF28",
|
||||
primary: "#21A821",
|
||||
primarydark: "#177317",
|
||||
successlight: "#1CFF1C",
|
||||
success: "#16CA16",
|
||||
successdark: "#0D910D",
|
||||
errorlight: "#FF3B3B",
|
||||
error: "#C32D2D",
|
||||
errordark: "#8E2121",
|
||||
secondarylight: "#B3B3B3",
|
||||
secondary: "#838383",
|
||||
secondarydark: "#676767",
|
||||
warninglight: "#FFFF3A",
|
||||
warning: "#C3C32A",
|
||||
warningdark: "#8C8C1E",
|
||||
infolight: "#64CBFF",
|
||||
info: "#3399CC",
|
||||
infodark: "#246D91",
|
||||
welllight: "#404040",
|
||||
well: "#1C1C1C",
|
||||
white: "#C3C3C3",
|
||||
black: "#0A0B0B",
|
||||
hp: "#C62E2E",
|
||||
money: "#D6BB27",
|
||||
hack: "#ADFF2F",
|
||||
combat: "#E8EDCD",
|
||||
cha: "#8B5FAF",
|
||||
int: "#537CC8",
|
||||
rep: "#E8EDCD",
|
||||
disabled: "#5AB5A5",
|
||||
backgroundprimary: "#0C0D0E",
|
||||
backgroundsecondary: "#121415",
|
||||
button: "#252829",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 111 KiB |
@@ -0,0 +1,44 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: 'Default',
|
||||
description: 'Default game theme, most supported',
|
||||
credit: 'hydroflame',
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#0f0",
|
||||
primary: "#0c0",
|
||||
primarydark: "#090",
|
||||
successlight: "#0f0",
|
||||
success: "#0c0",
|
||||
successdark: "#090",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 112 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Discord-like",
|
||||
description: "Discord inspired theme",
|
||||
credit: "Thermite",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924305252017143818",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#7389DC",
|
||||
primary: "#7389DC",
|
||||
primarydark: "#5964F1",
|
||||
successlight: "#00CC00",
|
||||
success: "#20DF20",
|
||||
successdark: "#0CB80C",
|
||||
errorlight: "#EA5558",
|
||||
error: "#EC4145",
|
||||
errordark: "#E82528",
|
||||
secondarylight: "#C3C3C3",
|
||||
secondary: "#9C9C9C",
|
||||
secondarydark: "#4E4E4E",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#1C4FB3",
|
||||
welllight: "#999999",
|
||||
well: "#35383C",
|
||||
white: "#FFFFFF",
|
||||
black: "#202225",
|
||||
hp: "#FF5656",
|
||||
money: "#43FF43",
|
||||
hack: "#FFAB3D",
|
||||
combat: "#8A90FD",
|
||||
cha: "#FF51D9",
|
||||
int: "#6495ed",
|
||||
rep: "#FFFF30",
|
||||
disabled: "#474B51",
|
||||
backgroundprimary: "#2F3136",
|
||||
backgroundsecondary: "#35393E",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Dracula",
|
||||
description: "Dracula Look-alike",
|
||||
credit: "H3draut3r",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922296307836678144",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#7082B8",
|
||||
primary: "#F8F8F2",
|
||||
primarydark: "#FF79C6",
|
||||
successlight: "#0f0",
|
||||
success: "#0c0",
|
||||
successdark: "#090",
|
||||
errorlight: "#FD4545",
|
||||
error: "#FF2D2D",
|
||||
errordark: "#C62424",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#8BE9FD",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#FFC281",
|
||||
warning: "#FFB86C",
|
||||
warningdark: "#E6A055",
|
||||
infolight: "#A0A0FF",
|
||||
info: "#7070FF",
|
||||
infodark: "#4040FF",
|
||||
welllight: "#44475A",
|
||||
well: "#363948",
|
||||
white: "#fff",
|
||||
black: "#282A36",
|
||||
hp: "#D34448",
|
||||
money: "#50FA7B",
|
||||
hack: "#F1FA8C",
|
||||
combat: "#BD93F9",
|
||||
cha: "#FF79C6",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#282A36",
|
||||
backgroundsecondary: "#21222C",
|
||||
button: "#21222C",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,14 @@
|
||||
export { Theme as Default } from "./default";
|
||||
export { Theme as DefaultLite } from "./default-lite";
|
||||
export { Theme as Monokai } from "./monokai-ish";
|
||||
export { Theme as Warmer } from "./warmer";
|
||||
export { Theme as DarkPlus } from "./dark-plus";
|
||||
export { Theme as MayukaiDark } from "./mayukai-dark";
|
||||
export { Theme as Purple } from "./purple";
|
||||
export { Theme as SmoothGreen } from "./smooth-green";
|
||||
export { Theme as Dracula } from "./dracula";
|
||||
export { Theme as DarkBlue } from "./dark-blue";
|
||||
export { Theme as DiscordLike } from "./discord-like";
|
||||
export { Theme as OneDark } from "./one-dark";
|
||||
export { Theme as MutedGoldBlue } from "./muted-gold-blue";
|
||||
export { Theme as Light } from "./light";
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Light",
|
||||
description: "Cobbled Together Light Theme",
|
||||
credit: "matt",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/926114005456658432",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#535353",
|
||||
primary: "#1A1A1A",
|
||||
primarydark: "#0d0d0d",
|
||||
successlight: "#63c439",
|
||||
success: "#428226",
|
||||
successdark: "#2E5A1B",
|
||||
errorlight: "#df7051",
|
||||
error: "#C94824",
|
||||
errordark: "#91341B",
|
||||
secondarylight: "#b3b3b3",
|
||||
secondary: "#9B9B9B",
|
||||
secondarydark: "#7A7979",
|
||||
warninglight: "#e8d464",
|
||||
warning: "#C6AD20",
|
||||
warningdark: "#9F8A16",
|
||||
infolight: "#6299cf",
|
||||
info: "#3778B7",
|
||||
infodark: "#30689C",
|
||||
welllight: "#f9f9f9",
|
||||
well: "#eaeaea",
|
||||
white: "#F7F7F7",
|
||||
black: "#F7F7F7",
|
||||
hp: "#BF5C41",
|
||||
money: "#E1B121",
|
||||
hack: "#47BC38",
|
||||
combat: "#656262",
|
||||
cha: "#A568AC",
|
||||
int: "#889BCF",
|
||||
rep: "#656262",
|
||||
disabled: "#70B4BF",
|
||||
backgroundprimary: "#F7F7F7",
|
||||
backgroundsecondary: "#f9f9f9",
|
||||
button: "#eaeaea",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Mayukai Dark",
|
||||
description: "Mayukai Dark-esque",
|
||||
credit: "Festive Noire",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922037502334889994",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#DDDFC5",
|
||||
primary: "#CDCFB6",
|
||||
primarydark: "#9D9F8C",
|
||||
successlight: "#00EF00",
|
||||
success: "#00A500",
|
||||
successdark: "#007A00",
|
||||
errorlight: "#F92672",
|
||||
error: "#CA1C5C",
|
||||
errordark: "#90274A",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#D3D300",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#00010A",
|
||||
white: "#fff",
|
||||
black: "#020509",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#8CCF27",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#080C11",
|
||||
backgroundsecondary: "#03080F",
|
||||
button: "#00010A",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1,44 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Monokai'ish",
|
||||
description: "Monokai'ish",
|
||||
credit: "eltea",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#FFF",
|
||||
primary: "#F8F8F2",
|
||||
primarydark: "#FAFAEB",
|
||||
successlight: "#ADE146",
|
||||
success: "#A6E22E",
|
||||
successdark: "#98E104",
|
||||
errorlight: "#FF69A0",
|
||||
error: "#F92672",
|
||||
errordark: "#D10F56",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E1D992",
|
||||
warning: "#E6DB74",
|
||||
warningdark: "#EDDD54",
|
||||
infolight: "#92E1F1",
|
||||
info: "#66D9EF",
|
||||
infodark: "#31CDED",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#F92672",
|
||||
money: "#E6DB74",
|
||||
hack: "#A6E22E",
|
||||
combat: "#75715E",
|
||||
cha: "#AE81FF",
|
||||
int: "#66D9EF",
|
||||
rep: "#E69F66",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#272822",
|
||||
backgroundsecondary: "#1B1C18",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Muted Gold & Blue",
|
||||
description: "Muted gold with blue accents.",
|
||||
credit: "Sloth",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924672660758208563",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#E3B54A",
|
||||
primary: "#CAA243",
|
||||
primarydark: "#7E6937",
|
||||
successlight: "#82FF82",
|
||||
success: "#6FDA6F",
|
||||
successdark: "#64C364",
|
||||
errorlight: "#FD5555",
|
||||
error: "#D84A4A",
|
||||
errordark: "#AC3939",
|
||||
secondarylight: "#D8D0B8",
|
||||
secondary: "#B1AA95",
|
||||
secondarydark: "#736E5E",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#111111",
|
||||
white: "#fff",
|
||||
black: "#070300",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#0A0A0E",
|
||||
backgroundsecondary: "#0E0E10",
|
||||
button: "#222222",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "One Dark",
|
||||
description: "Dark with a greenish tint",
|
||||
credit: "Dexalt142",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/924650660694208512",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#98C379",
|
||||
primary: "#98C379",
|
||||
primarydark: "#98C379",
|
||||
successlight: "#98C379",
|
||||
success: "#98C379",
|
||||
successdark: "#98C379",
|
||||
errorlight: "#E06C75",
|
||||
error: "#BE5046",
|
||||
errordark: "#BE5046",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E5C07B",
|
||||
warning: "#E5C07B",
|
||||
warningdark: "#D19A66",
|
||||
infolight: "#61AFEF",
|
||||
info: "#61AFEF",
|
||||
infodark: "#61AFEF",
|
||||
welllight: "#4B5263",
|
||||
well: "#282C34",
|
||||
white: "#ABB2BF",
|
||||
black: "#282C34",
|
||||
hp: "#E06C75",
|
||||
money: "#E5C07B",
|
||||
hack: "#98C379",
|
||||
combat: "#ABB2BF",
|
||||
cha: "#C678DD",
|
||||
int: "#61AFEF",
|
||||
rep: "#ABB2BF",
|
||||
disabled: "#56B6C2",
|
||||
backgroundprimary: "#282C34",
|
||||
backgroundsecondary: "#21252B",
|
||||
button: "#4B5263",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Purple",
|
||||
credit: "zer0ney",
|
||||
description: "Essentially all defaults except for purple replacing the main colors",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922091815849570395",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#BA55D3",
|
||||
primary: "#9370DB",
|
||||
primarydark: "#8A2BE2",
|
||||
successlight: "#BA55D3",
|
||||
success: "#9370DB",
|
||||
successdark: "#8A2BE2",
|
||||
errorlight: "#f00",
|
||||
error: "#c00",
|
||||
errordark: "#900",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#ff0",
|
||||
warning: "#cc0",
|
||||
warningdark: "#990",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Smooth Green",
|
||||
description: "A nice green theme that doesn't hurt your eyes.",
|
||||
credit: "Swidt",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/922243957986033725",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#E0E0BC",
|
||||
primary: "#B0D9A3",
|
||||
primarydark: "#B8B89C",
|
||||
successlight: "#00F000",
|
||||
success: "#6BC16B",
|
||||
successdark: "#00B400",
|
||||
errorlight: "#F00000",
|
||||
error: "#3D713D",
|
||||
errordark: "#A00000",
|
||||
secondarylight: "#B4AEAE",
|
||||
secondary: "#8FAF85",
|
||||
secondarydark: "#787272",
|
||||
warninglight: "#F0F000",
|
||||
warning: "#38F100",
|
||||
warningdark: "#A0A000",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#2F3C2B",
|
||||
white: "#fff",
|
||||
black: "#1E1E1E",
|
||||
hp: "#dd3434",
|
||||
money: "#4AA52E",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#a671d1",
|
||||
int: "#6495ed",
|
||||
rep: "#35A135",
|
||||
disabled: "#66cfbc",
|
||||
backgroundprimary: "#1E1E1E",
|
||||
backgroundsecondary: "#252525",
|
||||
button: "#2F3C2B",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,45 @@
|
||||
import { IPredefinedTheme } from "../../Themes";
|
||||
import img1 from "./screenshot.png";
|
||||
|
||||
export const Theme: IPredefinedTheme = {
|
||||
name: "Warmer",
|
||||
credit: "hexnaught",
|
||||
description: "Warmer, softer theme",
|
||||
reference: "https://discord.com/channels/415207508303544321/921991895230611466/921999581020028938",
|
||||
screenshot: img1,
|
||||
colors: {
|
||||
primarylight: "#EA9062",
|
||||
primary: "#DD7B4A",
|
||||
primarydark: "#D3591C",
|
||||
successlight: "#6ACF6A",
|
||||
success: "#43BF43",
|
||||
successdark: "#3E913E",
|
||||
errorlight: "#C15757",
|
||||
error: "#B34141",
|
||||
errordark: "#752525",
|
||||
secondarylight: "#AAA",
|
||||
secondary: "#888",
|
||||
secondarydark: "#666",
|
||||
warninglight: "#E6E69D",
|
||||
warning: "#DADA56",
|
||||
warningdark: "#A1A106",
|
||||
infolight: "#69f",
|
||||
info: "#36c",
|
||||
infodark: "#039",
|
||||
welllight: "#444",
|
||||
well: "#222",
|
||||
white: "#fff",
|
||||
black: "#000",
|
||||
hp: "#dd3434",
|
||||
money: "#ffd700",
|
||||
hack: "#adff2f",
|
||||
combat: "#faffdf",
|
||||
cha: "#AD84CF",
|
||||
int: "#6495ed",
|
||||
rep: "#faffdf",
|
||||
disabled: "#76C6B7",
|
||||
backgroundprimary: "#000",
|
||||
backgroundsecondary: "#000",
|
||||
button: "#333",
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 118 KiB |
@@ -0,0 +1,19 @@
|
||||
import React, { useState } from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import TextFormatIcon from "@mui/icons-material/TextFormat";
|
||||
import { StyleEditorModal } from "./StyleEditorModal";
|
||||
|
||||
export function StyleEditorButton(): React.ReactElement {
|
||||
const [styleEditorOpen, setStyleEditorOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="The style editor allows you to modify certain CSS rules used by the game.">
|
||||
<Button startIcon={<TextFormatIcon />} onClick={() => setStyleEditorOpen(true)}>
|
||||
Style Editor
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./Modal";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
@@ -11,7 +11,7 @@ import SaveIcon from "@mui/icons-material/Save";
|
||||
|
||||
import { ThemeEvents } from "./Theme";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { defaultStyles } from "../../Settings/Styles";
|
||||
import { defaultStyles } from "../Styles";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import { IStyleSettings } from "../../ScriptEditor/NetscriptDefinitions";
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import { ThemeEvents } from "./Theme";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { getPredefinedThemes, IPredefinedTheme } from "../Themes";
|
||||
import { Box, ButtonGroup, Button } from "@mui/material";
|
||||
import { IRouter } from "../../ui/Router";
|
||||
import { ThemeEditorButton } from "./ThemeEditorButton";
|
||||
import { StyleEditorButton } from "./StyleEditorButton";
|
||||
import { ThemeEntry } from "./ThemeEntry";
|
||||
import { ThemeCollaborate } from "./ThemeCollaborate";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||
|
||||
interface IProps {
|
||||
router: IRouter;
|
||||
}
|
||||
|
||||
// Everything dies when the theme gets reloaded, so we'll keep the current scroll to not jump around.
|
||||
let previousScrollY = 0;
|
||||
|
||||
export function ThemeBrowser({ router }: IProps): React.ReactElement {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalImageSrc, setModalImageSrc] = useState<string | undefined>();
|
||||
const predefinedThemes = getPredefinedThemes();
|
||||
const themes = (predefinedThemes &&
|
||||
Object.entries(predefinedThemes).map(([key, templateTheme]) => (
|
||||
<ThemeEntry
|
||||
key={key}
|
||||
theme={templateTheme}
|
||||
onActivated={() => setTheme(templateTheme)}
|
||||
onImageClick={handleZoom}
|
||||
/>
|
||||
))) || <></>;
|
||||
|
||||
function setTheme(theme: IPredefinedTheme): void {
|
||||
previousScrollY = window.scrollY;
|
||||
const previousColors = { ...Settings.theme };
|
||||
Object.assign(Settings.theme, theme.colors);
|
||||
ThemeEvents.emit();
|
||||
SnackbarEvents.emit(
|
||||
<>
|
||||
Updated theme to "<strong>{theme.name}</strong>"
|
||||
<Button
|
||||
sx={{ ml: 1 }}
|
||||
color="secondary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
Object.assign(Settings.theme, previousColors);
|
||||
ThemeEvents.emit();
|
||||
}}
|
||||
>
|
||||
UNDO
|
||||
</Button>
|
||||
</>,
|
||||
"info",
|
||||
30000,
|
||||
);
|
||||
}
|
||||
|
||||
function handleZoom(src: string): void {
|
||||
previousScrollY = window.scrollY;
|
||||
setModalImageSrc(src);
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
function handleCloseZoom(): void {
|
||||
previousScrollY = window.scrollY;
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => window.scrollTo(0, previousScrollY));
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ mx: 2 }}>
|
||||
<Typography variant="h4">Theme Browser</Typography>
|
||||
<Paper sx={{ px: 2, py: 1, my: 1 }}>
|
||||
<ThemeCollaborate />
|
||||
<ButtonGroup sx={{ mb: 2, display: "block" }}>
|
||||
<ThemeEditorButton router={router} />
|
||||
<StyleEditorButton />
|
||||
</ButtonGroup>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>{themes}</Box>
|
||||
<Modal open={modalOpen} onClose={handleCloseZoom}>
|
||||
<img src={modalImageSrc} style={{ width: "100%" }} />
|
||||
</Modal>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Link } from "@mui/material";
|
||||
|
||||
export function ThemeCollaborate(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ my: 1 }}>
|
||||
If you've created a theme that you believe should be added in game's theme browser, feel free to{" "}
|
||||
<Link href="https://github.com/danielyxie/bitburner/tree/dev/src/Themes/README.md" target="_blank">
|
||||
create a pull request
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
<Typography sx={{ my: 1 }}>
|
||||
Head over to the{" "}
|
||||
<Link href="https://discord.com/channels/415207508303544321/921991895230611466" target="_blank">
|
||||
theme-sharing
|
||||
</Link>{" "}
|
||||
discord channel for more.
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React, { useState } from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { ThemeEditorModal } from "./ThemeEditorModal";
|
||||
import { IRouter } from "../../ui/Router";
|
||||
import ColorizeIcon from "@mui/icons-material/Colorize";
|
||||
|
||||
interface IProps {
|
||||
router: IRouter;
|
||||
}
|
||||
|
||||
export function ThemeEditorButton({ router }: IProps): React.ReactElement {
|
||||
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="The theme editor allows you to modify the colors the game uses.">
|
||||
<Button id="bb-theme-editor-button" startIcon={<ColorizeIcon />} onClick={() => setThemeEditorOpen(true)}>
|
||||
Theme Editor
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} router={router} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Modal } from "./Modal";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Paper from "@mui/material/Paper";
|
||||
@@ -8,15 +9,19 @@ import TextField from "@mui/material/TextField";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ReplyIcon from "@mui/icons-material/Reply";
|
||||
import PaletteSharpIcon from "@mui/icons-material/PaletteSharp";
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
import { Color, ColorPicker } from "material-ui-color";
|
||||
import { ThemeEvents } from "./Theme";
|
||||
import { Settings, defaultSettings } from "../../Settings/Settings";
|
||||
import { getPredefinedThemes } from "../../Settings/Themes";
|
||||
import { defaultTheme } from "../Themes";
|
||||
import { UserInterfaceTheme } from "../../ScriptEditor/NetscriptDefinitions";
|
||||
import { IRouter } from "../../ui/Router";
|
||||
import { ThemeCollaborate } from "./ThemeCollaborate";
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
router: IRouter;
|
||||
}
|
||||
|
||||
interface IColorEditorProps {
|
||||
@@ -68,28 +73,6 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
|
||||
...Settings.theme,
|
||||
});
|
||||
|
||||
const predefinedThemes = getPredefinedThemes();
|
||||
const themes = predefinedThemes && Object.entries(predefinedThemes)
|
||||
.map(([key, templateTheme]) => {
|
||||
const name = templateTheme.name || key;
|
||||
let inner = <Typography>{name}</Typography>;
|
||||
let toolTipTitle;
|
||||
if (templateTheme.credit) {
|
||||
toolTipTitle = <Typography>{templateTheme.description || name} <em>by {templateTheme.credit}</em></Typography>;
|
||||
} else if (templateTheme.description) {
|
||||
toolTipTitle = <Typography>{templateTheme.description}</Typography>;
|
||||
}
|
||||
if (toolTipTitle) {
|
||||
inner = <Tooltip title={toolTipTitle}>{inner}</Tooltip>
|
||||
}
|
||||
return (
|
||||
<Button onClick={() => setTemplateTheme(templateTheme.colors)}
|
||||
startIcon={<PaletteSharpIcon />} key={key} sx={{ mr: 1, mb: 1 }}>
|
||||
{inner}
|
||||
</Button>
|
||||
);
|
||||
}) || <></>;
|
||||
|
||||
function setTheme(theme: UserInterfaceTheme): void {
|
||||
setCustomTheme(theme);
|
||||
Object.assign(Settings.theme, theme);
|
||||
@@ -372,8 +355,18 @@ export function ThemeEditorModal(props: IProps): React.ReactElement {
|
||||
/>
|
||||
<>
|
||||
<Typography sx={{ my: 1 }}>Backup your theme or share it with others by copying the string above.</Typography>
|
||||
<Typography sx={{ my: 1 }}>Replace the current theme with a pre-built template using the buttons below.</Typography>
|
||||
{themes}
|
||||
<ThemeCollaborate />
|
||||
<ButtonGroup>
|
||||
<Tooltip title="Reverts all modification back to the default theme. This is permanent.">
|
||||
<Button onClick={() => setTemplateTheme(defaultTheme)}
|
||||
startIcon={<HistoryIcon />}>
|
||||
Revert to Default
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Move over to the theme browser's page to use one of our predefined themes.">
|
||||
<Button startIcon={<PaletteSharpIcon />} onClick={() => props.router.toThemeBrowser()}>See more themes</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</>
|
||||
</Paper>
|
||||
</Modal>
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import PaletteSharpIcon from "@mui/icons-material/PaletteSharp";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { IPredefinedTheme } from "../Themes";
|
||||
import { Link, Card, CardHeader, CardContent, CardMedia, Button } from "@mui/material";
|
||||
|
||||
interface IProps {
|
||||
theme: IPredefinedTheme;
|
||||
onActivated: () => void;
|
||||
onImageClick: (src: string) => void;
|
||||
}
|
||||
|
||||
export function ThemeEntry({ theme, onActivated, onImageClick }: IProps): React.ReactElement {
|
||||
if (!theme) return <></>;
|
||||
return (
|
||||
<Card key={theme.screenshot} sx={{ width: 400, mr: 1, mb: 1 }}>
|
||||
<CardHeader
|
||||
action={
|
||||
<Tooltip title="Use this theme">
|
||||
<Button startIcon={<PaletteSharpIcon />} onClick={onActivated} variant="outlined">
|
||||
Use
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
title={theme.name}
|
||||
subheader={
|
||||
<>
|
||||
by {theme.credit}{" "}
|
||||
{theme.reference && (
|
||||
<>
|
||||
(
|
||||
<Link href={theme.reference} target="_blank">
|
||||
ref
|
||||
</Link>
|
||||
)
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
sx={{
|
||||
color: Settings.theme.primary,
|
||||
"& .MuiCardHeader-subheader": {
|
||||
color: Settings.theme.secondarydark,
|
||||
},
|
||||
"& .MuiButton-outlined": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<CardMedia
|
||||
component="img"
|
||||
width="400"
|
||||
image={theme.screenshot}
|
||||
alt={`Theme Screenshot of "${theme.name}"`}
|
||||
sx={{
|
||||
borderTop: `1px solid ${Settings.theme.welllight}`,
|
||||
borderBottom: `1px solid ${Settings.theme.welllight}`,
|
||||
cursor: "zoom-in",
|
||||
}}
|
||||
onClick={() => onImageClick(theme.screenshot)}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
color: Settings.theme.primarydark,
|
||||
}}
|
||||
>
|
||||
{theme.description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -916,8 +916,8 @@ export const codingContractTypesMetadata: ICodingContractTypeMetadata[] = [
|
||||
"You are given the following string which contains only digits between 0 and 9:\n\n",
|
||||
`${digits}\n\n`,
|
||||
`You are also given a target number of ${target}. Return all possible ways`,
|
||||
"you can add the +, -, and * operators to the string such that it evaluates",
|
||||
"to the target number.\n\n",
|
||||
"you can add the +(add), -(subtract), and *(multiply) operators to the string such",
|
||||
"that it evaluates to the target number. (Normal order of operations applies.)\n\n",
|
||||
"The provided answer should be an array of strings containing the valid expressions.",
|
||||
"The data provided by this problem is an array with two elements. The first element",
|
||||
"is the string of digits, while the second element is the target number:\n\n",
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Player } from "./Player";
|
||||
import { saveObject, loadGame } from "./SaveObject";
|
||||
import { initForeignServers } from "./Server/AllServers";
|
||||
import { Settings } from "./Settings/Settings";
|
||||
import { ThemeEvents } from "./ui/React/Theme";
|
||||
import { ThemeEvents } from "./Themes/ui/Theme";
|
||||
import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags";
|
||||
import { initSymbolToStockMap, processStockPrices } from "./StockMarket/StockMarket";
|
||||
import { Terminal } from "./Terminal";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import { TTheme as Theme, ThemeEvents, refreshTheme } from "./ui/React/Theme";
|
||||
import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme";
|
||||
import { LoadingScreen } from "./ui/LoadingScreen";
|
||||
import { initElectron } from "./Electron";
|
||||
initElectron();
|
||||
|
||||
@@ -72,6 +72,7 @@ import { LogBoxManager } from "./React/LogBoxManager";
|
||||
import { AlertManager } from "./React/AlertManager";
|
||||
import { PromptManager } from "./React/PromptManager";
|
||||
import { InvitationModal } from "../Faction/ui/InvitationModal";
|
||||
import { calculateAchievements } from "../Achievements/Achievements";
|
||||
|
||||
import { enterBitNode } from "../RedPill";
|
||||
import { Context } from "./Context";
|
||||
@@ -79,6 +80,12 @@ import { RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot";
|
||||
import { AchievementsRoot } from "../Achievements/AchievementsRoot";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
import { ThemeBrowser } from "../Themes/ui/ThemeBrowser";
|
||||
import { ImportSaveRoot } from "./React/ImportSaveRoot";
|
||||
import { BypassWrapper } from "./React/BypassWrapper";
|
||||
|
||||
import _wrap from "lodash/wrap";
|
||||
import _functions from "lodash/functions";
|
||||
|
||||
const htmlLocation = location;
|
||||
|
||||
@@ -107,6 +114,9 @@ export let Router: IRouter = {
|
||||
page: () => {
|
||||
throw new Error("Router called before initialization");
|
||||
},
|
||||
allowRouting: () => {
|
||||
throw new Error("Router called before initialization");
|
||||
},
|
||||
toActiveScripts: () => {
|
||||
throw new Error("Router called before initialization");
|
||||
},
|
||||
@@ -194,6 +204,12 @@ export let Router: IRouter = {
|
||||
toAchievements: () => {
|
||||
throw new Error("Router called before initialization");
|
||||
},
|
||||
toThemeBrowser: () => {
|
||||
throw new Error("Router called before initialization");
|
||||
},
|
||||
toImportSave: () => {
|
||||
throw new Error("Router called before initialization");
|
||||
},
|
||||
};
|
||||
|
||||
function determineStartPage(player: IPlayer): Page {
|
||||
@@ -223,6 +239,13 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
|
||||
const [errorBoundaryKey, setErrorBoundaryKey] = useState<number>(0);
|
||||
const [sidebarOpened, setSideBarOpened] = useState(Settings.IsSidebarOpened);
|
||||
|
||||
const [importString, setImportString] = useState<string>(undefined as unknown as string);
|
||||
const [importAutomatic, setImportAutomatic] = useState<boolean>(false);
|
||||
if (importString === undefined && page === Page.ImportSave)
|
||||
throw new Error("Trying to go to a page without the proper setup");
|
||||
|
||||
const [allowRoutingCalls, setAllowRoutingCalls] = useState(true);
|
||||
|
||||
function resetErrorBoundary(): void {
|
||||
setErrorBoundaryKey(errorBoundaryKey + 1);
|
||||
}
|
||||
@@ -244,6 +267,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
|
||||
|
||||
Router = {
|
||||
page: () => page,
|
||||
allowRouting: (value: boolean) => setAllowRoutingCalls(value),
|
||||
toActiveScripts: () => setPage(Page.ActiveScripts),
|
||||
toAugmentations: () => setPage(Page.Augmentations),
|
||||
toBladeburner: () => setPage(Page.Bladeburner),
|
||||
@@ -286,6 +310,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
|
||||
toBitVerse: (flume: boolean, quick: boolean) => {
|
||||
setFlume(flume);
|
||||
setQuick(quick);
|
||||
calculateAchievements();
|
||||
setPage(Page.BitVerse);
|
||||
},
|
||||
toInfiltration: (location: Location) => {
|
||||
@@ -307,8 +332,36 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
|
||||
toAchievements: () => {
|
||||
setPage(Page.Achievements);
|
||||
},
|
||||
toThemeBrowser: () => {
|
||||
setPage(Page.ThemeBrowser);
|
||||
},
|
||||
toImportSave: (base64save: string, automatic = false) => {
|
||||
setImportString(base64save);
|
||||
setImportAutomatic(automatic);
|
||||
setPage(Page.ImportSave);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// Wrap Router navigate functions to be able to disable the execution
|
||||
_functions(Router).
|
||||
filter((fnName) => fnName.startsWith('to')).
|
||||
forEach((fnName) => {
|
||||
// @ts-ignore - tslint does not like this, couldn't find a way to make it cooperate
|
||||
Router[fnName] = _wrap(Router[fnName], (func, ...args) => {
|
||||
if (!allowRoutingCalls) {
|
||||
// Let's just log to console.
|
||||
console.log(`Routing is currently disabled - Attempted router.${fnName}()`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the function normally
|
||||
return func(...args);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (page !== Page.Terminal) window.scrollTo(0, 0);
|
||||
});
|
||||
@@ -323,11 +376,13 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
|
||||
let mainPage = <Typography>Cannot load</Typography>;
|
||||
let withSidebar = true;
|
||||
let withPopups = true;
|
||||
let bypassGame = false;
|
||||
switch (page) {
|
||||
case Page.Recovery: {
|
||||
mainPage = <RecoveryRoot router={Router} softReset={softReset} />;
|
||||
withSidebar = false;
|
||||
withPopups = false;
|
||||
bypassGame = true;
|
||||
break;
|
||||
}
|
||||
case Page.BitVerse: {
|
||||
@@ -471,6 +526,7 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
|
||||
mainPage = (
|
||||
<GameOptionsRoot
|
||||
player={player}
|
||||
router={Router}
|
||||
save={() => saveObject.saveGame()}
|
||||
export={() => {
|
||||
// Apply the export bonus before saving the game
|
||||
@@ -503,44 +559,66 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
|
||||
mainPage = <AchievementsRoot />;
|
||||
break;
|
||||
}
|
||||
case Page.ThemeBrowser: {
|
||||
mainPage = <ThemeBrowser router={Router} />;
|
||||
break;
|
||||
}
|
||||
case Page.ImportSave: {
|
||||
mainPage = (
|
||||
<ImportSaveRoot
|
||||
importString={importString}
|
||||
automatic={importAutomatic}
|
||||
router={Router}
|
||||
/>
|
||||
);
|
||||
withSidebar = false;
|
||||
withPopups = false;
|
||||
bypassGame = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Context.Player.Provider value={player}>
|
||||
<Context.Router.Provider value={Router}>
|
||||
<ErrorBoundary key={errorBoundaryKey} router={Router} softReset={softReset}>
|
||||
<SnackbarProvider>
|
||||
<Overview mode={ITutorial.isRunning ? "tutorial" : "overview"}>
|
||||
{!ITutorial.isRunning ? (
|
||||
<CharacterOverview save={() => saveObject.saveGame()} killScripts={killAllScripts} />
|
||||
<BypassWrapper content={bypassGame ? mainPage : null}>
|
||||
<SnackbarProvider>
|
||||
<Overview mode={ITutorial.isRunning ? "tutorial" : "overview"}>
|
||||
{!ITutorial.isRunning ? (
|
||||
<CharacterOverview save={() => saveObject.saveGame()} killScripts={killAllScripts} />
|
||||
) : (
|
||||
<InteractiveTutorialRoot />
|
||||
)}
|
||||
</Overview>
|
||||
{withSidebar ? (
|
||||
<Box display="flex" flexDirection="row" width="100%">
|
||||
<SidebarRoot
|
||||
player={player}
|
||||
router={Router}
|
||||
page={page}
|
||||
opened={sidebarOpened}
|
||||
onToggled={(isOpened) => {
|
||||
setSideBarOpened(isOpened);
|
||||
Settings.IsSidebarOpened = isOpened;
|
||||
}}
|
||||
/>
|
||||
<Box className={classes.root}>{mainPage}</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<InteractiveTutorialRoot />
|
||||
)}
|
||||
</Overview>
|
||||
{withSidebar ? (
|
||||
<Box display="flex" flexDirection="row" width="100%">
|
||||
<SidebarRoot player={player} router={Router} page={page}
|
||||
opened={sidebarOpened}
|
||||
onToggled={(isOpened) => {
|
||||
setSideBarOpened(isOpened);
|
||||
Settings.IsSidebarOpened = isOpened;
|
||||
}} />
|
||||
<Box className={classes.root}>{mainPage}</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Box className={classes.root}>{mainPage}</Box>
|
||||
)}
|
||||
<Unclickable />
|
||||
{withPopups && (
|
||||
<>
|
||||
<LogBoxManager />
|
||||
<AlertManager />
|
||||
<PromptManager />
|
||||
<InvitationModal />
|
||||
<Snackbar />
|
||||
</>
|
||||
)}
|
||||
</SnackbarProvider>
|
||||
)}
|
||||
<Unclickable />
|
||||
{withPopups && (
|
||||
<>
|
||||
<LogBoxManager />
|
||||
<AlertManager />
|
||||
<PromptManager />
|
||||
<InvitationModal />
|
||||
<Snackbar />
|
||||
</>
|
||||
)}
|
||||
</SnackbarProvider>
|
||||
</BypassWrapper>
|
||||
</ErrorBoundary>
|
||||
</Context.Router.Provider>
|
||||
</Context.Player.Provider>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { GameRoot } from "./GameRoot";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
import { ActivateRecoveryMode } from "./React/RecoveryRoot";
|
||||
import { hash } from "../hash/hash";
|
||||
import { pushGameReady } from "../Electron";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -56,6 +57,7 @@ export function LoadingScreen(): React.ReactElement {
|
||||
throw err;
|
||||
}
|
||||
|
||||
pushGameReady();
|
||||
setLoaded(true);
|
||||
})
|
||||
.catch((reason) => {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
export function BypassWrapper(props: IProps): React.ReactElement {
|
||||
if (!props.content) return <>{props.children}</>;
|
||||
return <>{props.content}</>;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ interface IProps {
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
confirmationText: string | React.ReactNode;
|
||||
additionalButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ConfirmationModal(props: IProps): React.ReactElement {
|
||||
@@ -23,6 +24,7 @@ export function ConfirmationModal(props: IProps): React.ReactElement {
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
{props.additionalButton && <>{props.additionalButton}</>}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import Button from "@mui/material/Button";
|
||||
import { Tooltip } from '@mui/material';
|
||||
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { pushDisableRestore } from '../../Electron';
|
||||
|
||||
interface IProps {
|
||||
color?: "primary" | "warning" | "error";
|
||||
@@ -21,7 +22,10 @@ export function DeleteGameButton({ color = "primary" }: IProps): React.ReactElem
|
||||
onConfirm={() => {
|
||||
setModalOpened(false);
|
||||
deleteGame()
|
||||
.then(() => setTimeout(() => location.reload(), 1000))
|
||||
.then(() => {
|
||||
pushDisableRestore();
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
})
|
||||
.catch((r) => console.error(`Could not delete game: ${r}`));
|
||||
}}
|
||||
open={modalOpened}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { AlertEvents } from "./AlertManager";
|
||||
|
||||
import React from "react";
|
||||
import { SxProps } from "@mui/system";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
export function dialogBoxCreate(txt: string | JSX.Element): void {
|
||||
export function dialogBoxCreate(txt: string | JSX.Element, styles?: SxProps): void {
|
||||
if (typeof txt !== "string") {
|
||||
AlertEvents.emit(txt);
|
||||
} else {
|
||||
AlertEvents.emit(<span dangerouslySetInnerHTML={{ __html: txt }} />);
|
||||
AlertEvents.emit(<Typography component="span" sx={styles} dangerouslySetInnerHTML={{ __html: txt }} />);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,21 +22,23 @@ import TextField from "@mui/material/TextField";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import UploadIcon from "@mui/icons-material/Upload";
|
||||
import SaveIcon from "@mui/icons-material/Save";
|
||||
import PaletteIcon from "@mui/icons-material/Palette";
|
||||
|
||||
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
|
||||
import { dialogBoxCreate } from "./DialogBox";
|
||||
import { ConfirmationModal } from "./ConfirmationModal";
|
||||
import { ThemeEditorModal } from "./ThemeEditorModal";
|
||||
import { StyleEditorModal } from "./StyleEditorModal";
|
||||
|
||||
import { SnackbarEvents } from "./Snackbar";
|
||||
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { save } from "../../db";
|
||||
import { formatTime } from "../../utils/helpers/formatTime";
|
||||
import { OptionSwitch } from "./OptionSwitch";
|
||||
import { DeleteGameButton } from "./DeleteGameButton";
|
||||
import { SoftResetButton } from "./SoftResetButton";
|
||||
import { IRouter } from "../Router";
|
||||
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
|
||||
import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
|
||||
import { formatTime } from "../../utils/helpers/formatTime";
|
||||
import { OptionSwitch } from "./OptionSwitch";
|
||||
import { ImportData, saveObject } from "../../SaveObject";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -50,18 +52,13 @@ const useStyles = makeStyles((theme: Theme) =>
|
||||
|
||||
interface IProps {
|
||||
player: IPlayer;
|
||||
router: IRouter;
|
||||
save: () => void;
|
||||
export: () => void;
|
||||
forceKill: () => void;
|
||||
softReset: () => void;
|
||||
}
|
||||
|
||||
interface ImportData {
|
||||
base64: string;
|
||||
parsed: any;
|
||||
exportDate?: Date;
|
||||
}
|
||||
|
||||
export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const importInput = useRef<HTMLInputElement>(null);
|
||||
@@ -75,8 +72,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
|
||||
const [locale, setLocale] = useState(Settings.Locale);
|
||||
const [diagnosticOpen, setDiagnosticOpen] = useState(false);
|
||||
const [themeEditorOpen, setThemeEditorOpen] = useState(false);
|
||||
const [styleEditorOpen, setStyleEditorOpen] = useState(false);
|
||||
const [importSaveOpen, setImportSaveOpen] = useState(false);
|
||||
const [importData, setImportData] = useState<ImportData | null>(null);
|
||||
|
||||
@@ -127,78 +122,35 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
ii.click();
|
||||
}
|
||||
|
||||
function onImport(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
const files = event.target.files;
|
||||
if (files === null) return;
|
||||
const file = files[0];
|
||||
if (!file) {
|
||||
dialogBoxCreate("Invalid file selected");
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (this: FileReader, e: ProgressEvent<FileReader>) {
|
||||
const target = e.target;
|
||||
if (target === null) {
|
||||
console.error("error importing file");
|
||||
return;
|
||||
}
|
||||
const result = target.result;
|
||||
if (typeof result !== "string" || result === null) {
|
||||
console.error("FileReader event was not type string");
|
||||
return;
|
||||
}
|
||||
const contents = result;
|
||||
|
||||
let newSave;
|
||||
try {
|
||||
newSave = window.atob(contents);
|
||||
newSave = newSave.trim();
|
||||
} catch (error) {
|
||||
console.log(error); // We'll handle below
|
||||
}
|
||||
|
||||
if (!newSave || newSave === "") {
|
||||
SnackbarEvents.emit("Save game had not content or was not base64 encoded", "error", 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedSave;
|
||||
try {
|
||||
parsedSave = JSON.parse(newSave);
|
||||
} catch (error) {
|
||||
console.log(error); // We'll handle below
|
||||
}
|
||||
|
||||
if (!parsedSave || parsedSave.ctor !== "BitburnerSaveObject" || !parsedSave.data) {
|
||||
SnackbarEvents.emit("Save game did not seem valid", "error", 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
const data: ImportData = {
|
||||
base64: contents,
|
||||
parsed: parsedSave,
|
||||
};
|
||||
|
||||
const timestamp = parsedSave.data.SaveTimestamp;
|
||||
if (timestamp && timestamp !== "0") {
|
||||
data.exportDate = new Date(parseInt(timestamp, 10));
|
||||
}
|
||||
|
||||
async function onImport(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
|
||||
try {
|
||||
const base64Save = await saveObject.getImportStringFromFile(event.target.files);
|
||||
const data = await saveObject.getImportDataFromString(base64Save);
|
||||
setImportData(data);
|
||||
setImportSaveOpen(true);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} catch (ex: any) {
|
||||
SnackbarEvents.emit(ex.toString(), "error", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmedImportGame(): void {
|
||||
async function confirmedImportGame(): Promise<void> {
|
||||
if (!importData) return;
|
||||
|
||||
try {
|
||||
await saveObject.importGame(importData.base64);
|
||||
} catch (ex: any) {
|
||||
SnackbarEvents.emit(ex.toString(), "error", 5000);
|
||||
}
|
||||
|
||||
setImportSaveOpen(false);
|
||||
save(importData.base64).then(() => {
|
||||
setImportData(null);
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
});
|
||||
setImportData(null);
|
||||
}
|
||||
|
||||
function compareSaveGame(): void {
|
||||
if (!importData) return;
|
||||
props.router.toImportSave(importData.base64);
|
||||
setImportSaveOpen(false);
|
||||
setImportData(null);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -211,123 +163,115 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
<Grid item xs={12} sm={6}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The minimum number of milliseconds it takes to execute an operation in Netscript. Setting this too
|
||||
low can result in poor performance if you have many scripts running.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>.script exec time (ms)</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={execTime}
|
||||
onChange={handleExecTimeChange}
|
||||
step={1}
|
||||
min={5}
|
||||
max={100}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of recently killed script entries being tracked. Setting this too high can cause
|
||||
the game to use a lot of memory.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Recently killed scripts size</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={recentScriptsSize}
|
||||
onChange={handleRecentScriptsSizeChange}
|
||||
step={25}
|
||||
min={25}
|
||||
max={500}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of lines a script's logs can hold. Setting this too high can cause the game to
|
||||
use a lot of memory if you have many scripts running.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Netscript log size</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={logSize}
|
||||
onChange={handleLogSizeChange}
|
||||
step={20}
|
||||
min={20}
|
||||
max={500}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of entries that can be written to a port using Netscript's write() function.
|
||||
Setting this too high can cause the game to use a lot of memory.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Netscript port size</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={portSize}
|
||||
onChange={handlePortSizeChange}
|
||||
step={1}
|
||||
min={20}
|
||||
max={100}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of entries that can be written to the terminal. Setting this too high can cause
|
||||
the game to use a lot of memory.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Terminal capacity</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={terminalSize}
|
||||
onChange={handleTerminalSizeChange}
|
||||
step={50}
|
||||
min={50}
|
||||
max={500}
|
||||
valueLabelDisplay="auto"
|
||||
marks
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>The time (in seconds) between each autosave. Set to 0 to disable autosave.</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Autosave interval (s)</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={autosaveInterval}
|
||||
onChange={handleAutosaveIntervalChange}
|
||||
step={30}
|
||||
min={0}
|
||||
max={600}
|
||||
valueLabelDisplay="auto"
|
||||
marks
|
||||
/>
|
||||
<Box display="grid" sx={{ width: "fit-content", gridTemplateColumns: "1fr 3.5fr", gap: 1 }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The minimum number of milliseconds it takes to execute an operation in Netscript. Setting this too
|
||||
low can result in poor performance if you have many scripts running.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>.script exec time (ms)</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={execTime}
|
||||
onChange={handleExecTimeChange}
|
||||
step={1}
|
||||
min={5}
|
||||
max={100}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of recently killed script entries being tracked. Setting this too high can
|
||||
cause the game to use a lot of memory.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Recently killed scripts size</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={recentScriptsSize}
|
||||
onChange={handleRecentScriptsSizeChange}
|
||||
step={25}
|
||||
min={25}
|
||||
max={500}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of lines a script's logs can hold. Setting this too high can cause the game to
|
||||
use a lot of memory if you have many scripts running.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Netscript log size</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={logSize}
|
||||
onChange={handleLogSizeChange}
|
||||
step={20}
|
||||
min={20}
|
||||
max={500}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of entries that can be written to a port using Netscript's write() function.
|
||||
Setting this too high can cause the game to use a lot of memory.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Netscript port size</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={portSize}
|
||||
onChange={handlePortSizeChange}
|
||||
step={1}
|
||||
min={20}
|
||||
max={100}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
The maximum number of entries that can be written to the terminal. Setting this too high can cause
|
||||
the game to use a lot of memory.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Terminal capacity</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={terminalSize}
|
||||
onChange={handleTerminalSizeChange}
|
||||
step={50}
|
||||
min={50}
|
||||
max={500}
|
||||
valueLabelDisplay="auto"
|
||||
marks
|
||||
/>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>The time (in seconds) between each autosave. Set to 0 to disable autosave.</Typography>
|
||||
}
|
||||
>
|
||||
<Typography>Autosave interval (s)</Typography>
|
||||
</Tooltip>
|
||||
<Slider
|
||||
value={autosaveInterval}
|
||||
onChange={handleAutosaveIntervalChange}
|
||||
step={30}
|
||||
min={0}
|
||||
max={600}
|
||||
valueLabelDisplay="auto"
|
||||
marks
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<OptionSwitch
|
||||
@@ -616,6 +560,7 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
open={importSaveOpen}
|
||||
onClose={() => setImportSaveOpen(false)}
|
||||
onConfirm={() => confirmedImportGame()}
|
||||
additionalButton={<Button onClick={compareSaveGame}>Compare Save</Button>}
|
||||
confirmationText={
|
||||
<>
|
||||
Importing a new game will <strong>completely wipe</strong> the current data!
|
||||
@@ -624,15 +569,24 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
Make sure to have a backup of your current save file before importing.
|
||||
<br />
|
||||
The file you are attempting to import seems valid.
|
||||
<br />
|
||||
<br />
|
||||
{importData?.exportDate && (
|
||||
{(importData?.playerData?.lastSave ?? 0) > 0 && (
|
||||
<>
|
||||
The export date of the save file is <strong>{importData?.exportDate.toString()}</strong>
|
||||
<br />
|
||||
<br />
|
||||
The export date of the save file is{" "}
|
||||
<strong>{new Date(importData?.playerData?.lastSave ?? 0).toLocaleString()}</strong>
|
||||
</>
|
||||
)}
|
||||
{(importData?.playerData?.totalPlaytime ?? 0) > 0 && (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
Total play time of imported game:{" "}
|
||||
{convertTimeMsToTimeElapsedString(importData?.playerData?.totalPlaytime ?? 0)}
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -668,9 +622,14 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
<Button onClick={() => setDiagnosticOpen(true)}>Diagnose files</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
|
||||
<Button onClick={() => setThemeEditorOpen(true)}>Theme editor</Button>
|
||||
<Button onClick={() => setStyleEditorOpen(true)}>Style editor</Button>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
|
||||
<Tooltip title="Head to the theme browser to see a collection of prebuilt themes.">
|
||||
<Button startIcon={<PaletteIcon />} onClick={() => props.router.toThemeBrowser()}>
|
||||
Theme Browser
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<ThemeEditorButton router={props.router} />
|
||||
<StyleEditorButton />
|
||||
</Box>
|
||||
<Box>
|
||||
<Link href="https://github.com/danielyxie/bitburner/issues/new" target="_blank">
|
||||
@@ -695,8 +654,6 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
|
||||
</Box>
|
||||
</Grid>
|
||||
<FileDiagnosticModal open={diagnosticOpen} onClose={() => setDiagnosticOpen(false)} />
|
||||
<ThemeEditorModal open={themeEditorOpen} onClose={() => setThemeEditorOpen(false)} />
|
||||
<StyleEditorModal open={styleEditorOpen} onClose={() => setStyleEditorOpen(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||