MISC: enforce eslint react checks (#640)

This commit is contained in:
Aleksei Bezrodnov 2023-06-27 04:29:44 +02:00 committed by GitHub
parent 91bfb154b6
commit 1d5a735941
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2062 additions and 601 deletions

@ -7,6 +7,8 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
//"plugin:@typescript-eslint/recommended-requiring-type-checking",
//"plugin:@typescript-eslint/strict",
],
@ -31,5 +33,6 @@ module.exports = {
],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"react/no-unescaped-entities": "off",
},
};

1707
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -71,7 +71,9 @@
"css-loader": "^6.7.3",
"electron": "^22.2.1",
"electron-packager": "^17.1.1",
"eslint": "^8.31.0",
"eslint": "^8.43.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.14",
"html-webpack-plugin": "^5.5.0",

@ -83,13 +83,16 @@ export function SourceFilesElement(): React.ReactElement {
sfList.sort(([n1, __lvl1], [n2, __lvl2]) => n1 - n2);
}
if (sfList.length === 0) {
const [selectedSf, setSelectedSf] = useState(() => {
if (sfList.length === 0) return null;
const [n, lvl] = sfList[0];
return { n, lvl };
});
if (!selectedSf) {
return <></>;
}
const firstEle = sfList[0];
const [selectedSf, setSelectedSf] = useState({ n: firstEle[0], lvl: firstEle[1] });
return (
<Box sx={{ width: "100%", mt: 1 }}>
<Paper sx={{ p: 1 }}>

@ -1,10 +1,11 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { Box, Button, Paper, Tooltip, Typography } from "@mui/material";
import { Player } from "@player";
import { FactionName } from "@enums";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { BladeburnerConstants } from "../data/Constants";
import { Money } from "../../ui/React/Money";
import { useRerender } from "../../ui/React/hooks";
import { formatNumberNoSuffix, formatPopulation, formatBigNumber } from "../../ui/formatNumber";
import { Factions } from "../../Faction/Factions";
import { Router } from "../../ui/GameRoot";
@ -20,13 +21,9 @@ interface IProps {
export function Stats(props: IProps): React.ReactElement {
const [travelOpen, setTravelOpen] = useState(false);
const setRerender = useState(false)[1];
useRerender(1000);
const inFaction = props.bladeburner.rank >= BladeburnerConstants.RankNeededForFaction;
useEffect(() => {
const id = setInterval(() => setRerender((old) => !old), 1000);
return () => clearInterval(id);
}, []);
function openFaction(): void {
if (!inFaction) return;

@ -151,7 +151,7 @@ export class CodingContract {
/** Creates a popup to prompt the player to solve the problem */
async prompt(): Promise<CodingContractResult> {
return new Promise<CodingContractResult>((resolve) => {
const props = {
CodingContractEvent.emit({
c: this,
onClose: () => {
resolve(CodingContractResult.Cancelled);
@ -163,8 +163,7 @@ export class CodingContract {
resolve(CodingContractResult.Failure);
}
},
};
CodingContractEvent.emit(props);
});
});
}

@ -14,10 +14,12 @@ import { useRerender } from "../../ui/React/hooks";
export function CorporationRoot(): React.ReactElement {
const rerender = useRerender(200);
const [divisionName, setDivisionName] = useState<string | number>("Overview");
const corporation = Player.corporation;
if (corporation === null) return <></>;
const [divisionName, setDivisionName] = useState<string | number>("Overview");
function handleChange(event: React.SyntheticEvent, tab: string | number): void {
function handleChange(_event: React.SyntheticEvent, tab: string | number): void {
setDivisionName(tab);
}

@ -151,9 +151,9 @@ export function DivisionOverview(props: DivisionOverviewProps): React.ReactEleme
<br />
<StatsTable
rows={[
["Revenue:", <MoneyRate money={division.lastCycleRevenue} />],
["Expenses:", <MoneyRate money={division.lastCycleExpenses} />],
["Profit:", <MoneyRate money={profit} />],
["Revenue:", <MoneyRate key="revenue" money={division.lastCycleRevenue} />],
["Expenses:", <MoneyRate key="expenses" money={division.lastCycleExpenses} />],
["Profit:", <MoneyRate key="profit" money={profit} />],
]}
/>
<br />

@ -40,6 +40,7 @@ const useStyles = makeStyles(() =>
);
function WarehouseRoot(props: WarehouseProps): React.ReactElement {
const classes = useStyles();
const corp = useCorporation();
const division = useDivision();
const [smartSupplyOpen, setSmartSupplyOpen] = useState(false);
@ -57,8 +58,6 @@ function WarehouseRoot(props: WarehouseProps): React.ReactElement {
props.rerender();
}
const classes = useStyles();
// Current State:
let stateText;
switch (division.state) {

@ -61,13 +61,13 @@ export function Overview({ rerender }: IProps): React.ReactElement {
<>
<StatsTable
rows={[
["Total Funds:", <Money money={corp.funds} />],
["Total Revenue:", <MoneyRate money={corp.revenue} />],
["Total Expenses:", <MoneyRate money={corp.expenses} />],
["Total Profit:", <MoneyRate money={corp.revenue - corp.expenses} />],
["Total Funds:", <Money key="funds" money={corp.funds} />],
["Total Revenue:", <MoneyRate key="revenue" money={corp.revenue} />],
["Total Expenses:", <MoneyRate key="expenses" money={corp.expenses} />],
["Total Profit:", <MoneyRate key="profit" money={corp.revenue - corp.expenses} />],
["Publicly Traded:", corp.public ? "Yes" : "No"],
["Owned Stock Shares:", formatShares(corp.numShares)],
["Stock Price:", corp.public ? <Money money={corp.sharePrice} /> : "N/A"],
["Stock Price:", corp.public ? <Money key="price" money={corp.sharePrice} /> : "N/A"],
]}
/>
<br />
@ -160,16 +160,16 @@ interface IUpgradeProps {
}
// Render the UI for Corporation upgrades
function Upgrades({ rerender }: IUpgradeProps): React.ReactElement {
const [purchaseMultiplier, setPurchaseMultiplier] = useState<PositiveInteger | "MAX">(
corpConstants.PurchaseMultipliers.x1,
);
const corp = useCorporation();
// Don't show upgrades
if (corp.divisions.size === 0) {
return <Typography variant="h4">Upgrades are unlocked once you create an industry.</Typography>;
}
const [purchaseMultiplier, setPurchaseMultiplier] = useState<PositiveInteger | "MAX">(
corpConstants.PurchaseMultipliers.x1,
);
const unlocksNotOwned = Object.values(CorpUnlocks)
.filter((unlock) => !corp.unlocks.has(unlock.name))
.map(({ name }) => <Unlock rerender={rerender} name={name} key={name} />);
@ -332,10 +332,10 @@ function DividendsStats({ profit }: IDividendsStatsProps): React.ReactElement {
return (
<StatsTable
rows={[
["Retained Profits (after dividends):", <MoneyRate money={retainedEarnings} />],
["Retained Profits (after dividends):", <MoneyRate key="profits" money={retainedEarnings} />],
["Dividend Percentage:", formatPercent(corp.dividendRate, 0)],
["Dividends per share:", <MoneyRate money={dividendsPerShare} />],
["Your earnings as a shareholder:", <MoneyRate money={playerEarnings} />],
["Dividends per share:", <MoneyRate key="dividends" money={dividendsPerShare} />],
["Your earnings as a shareholder:", <MoneyRate key="earnings" money={playerEarnings} />],
]}
/>
);

@ -13,9 +13,10 @@ interface IMarketTA2Props {
}
function MarketTA2(props: IMarketTA2Props): React.ReactElement {
const rerender = useRerender();
const division = useDivision();
if (!division.hasResearch("Market-TA.II")) return <></>;
const rerender = useRerender();
function onMarketTA2(event: React.ChangeEvent<HTMLInputElement>): void {
props.mat.marketTa2 = event.target.checked;

@ -13,9 +13,10 @@ interface ITa2Props {
}
function MarketTA2(props: ITa2Props): React.ReactElement {
const rerender = useRerender();
const division = useDivision();
if (!division.hasResearch("Market-TA.II")) return <></>;
const rerender = useRerender();
function onCheckedChange(event: React.ChangeEvent<HTMLInputElement>): void {
props.product.marketTa2 = event.target.checked;

@ -19,7 +19,7 @@ interface IProps {
export function StaneksGiftRoot({ staneksGift }: IProps): React.ReactElement {
const rerender = useRerender();
useEffect(() => StaneksGiftEvents.subscribe(rerender), []);
useEffect(() => StaneksGiftEvents.subscribe(rerender), [rerender]);
return (
<Container maxWidth="lg" disableGutters sx={{ mx: 0 }}>
<Typography variant="h4">

@ -1,28 +1,31 @@
import React, { useEffect } from "react";
import Typography from "@mui/material/Typography";
import { Player } from "@player";
import { AugmentationName } from "@enums";
import React, { useEffect } from "react";
import { General } from "./DevMenu/ui/General";
import { Stats } from "./DevMenu/ui/Stats";
import { FactionsDev } from "./DevMenu/ui/FactionsDev";
import { Augmentations } from "./DevMenu/ui/Augmentations";
import { SourceFiles } from "./DevMenu/ui/SourceFiles";
import { Programs } from "./DevMenu/ui/Programs";
import { Servers } from "./DevMenu/ui/Servers";
import { Companies } from "./DevMenu/ui/Companies";
import { Bladeburner as BladeburnerElem } from "./DevMenu/ui/Bladeburner";
import { Gang } from "./DevMenu/ui/Gang";
import { Corporation } from "./DevMenu/ui/Corporation";
import { CodingContracts } from "./DevMenu/ui/CodingContracts";
import { StockMarket } from "./DevMenu/ui/StockMarket";
import { Sleeves } from "./DevMenu/ui/Sleeves";
import { Stanek } from "./DevMenu/ui/Stanek";
import { TimeSkip } from "./DevMenu/ui/TimeSkip";
import { SaveFile } from "./DevMenu/ui/SaveFile";
import { Achievements } from "./DevMenu/ui/Achievements";
import { Entropy } from "./DevMenu/ui/Entropy";
import Typography from "@mui/material/Typography";
import { StatsDev } from "./DevMenu/ui/StatsDev";
import { FactionsDev } from "./DevMenu/ui/FactionsDev";
import { AugmentationsDev } from "./DevMenu/ui/AugmentationsDev";
import { SourceFilesDev } from "./DevMenu/ui/SourceFilesDev";
import { ProgramsDev } from "./DevMenu/ui/ProgramsDev";
import { ServersDev } from "./DevMenu/ui/ServersDev";
import { CompaniesDev } from "./DevMenu/ui/CompaniesDev";
import { BladeburnerDev } from "./DevMenu/ui/BladeburnerDev";
import { GangDev } from "./DevMenu/ui/GangDev";
import { CorporationDev } from "./DevMenu/ui/CorporationDev";
import { CodingContractsDev } from "./DevMenu/ui/CodingContractsDev";
import { StockMarketDev } from "./DevMenu/ui/StockMarketDev";
import { SleevesDev } from "./DevMenu/ui/SleevesDev";
import { StanekDev } from "./DevMenu/ui/StanekDev";
import { SaveFileDev } from "./DevMenu/ui/SaveFileDev";
import { AchievementsDev } from "./DevMenu/ui/AchievementsDev";
import { EntropyDev } from "./DevMenu/ui/EntropyDev";
import { Exploit } from "./Exploits/Exploit";
export function DevMenuRoot(): React.ReactElement {
@ -33,31 +36,31 @@ export function DevMenuRoot(): React.ReactElement {
<>
<Typography>Development Menu - Only meant to be used for testing/debugging</Typography>
<General />
<Stats />
<StatsDev />
<FactionsDev />
<Augmentations />
<SourceFiles />
<Programs />
<Servers />
<Companies />
<AugmentationsDev />
<SourceFilesDev />
<ProgramsDev />
<ServersDev />
<CompaniesDev />
{Player.bladeburner && <BladeburnerElem />}
{Player.bladeburner && <BladeburnerDev bladeburner={Player.bladeburner} />}
{Player.gang && <Gang />}
{Player.gang && <GangDev />}
{Player.corporation && <Corporation />}
{Player.corporation && <CorporationDev />}
<CodingContracts />
<CodingContractsDev />
{Player.hasWseAccount && <StockMarket />}
{Player.hasWseAccount && <StockMarketDev />}
{Player.sleeves.length > 0 && <Sleeves />}
{Player.augmentations.some((aug) => aug.name === AugmentationName.StaneksGift1) && <Stanek />}
{Player.sleeves.length > 0 && <SleevesDev />}
{Player.augmentations.some((aug) => aug.name === AugmentationName.StaneksGift1) && <StanekDev />}
<TimeSkip />
<Achievements />
<Entropy />
<SaveFile />
<AchievementsDev />
<EntropyDev />
<SaveFileDev />
</>
);
}

@ -15,7 +15,7 @@ import { Player } from "@player";
import { achievements } from "../../Achievements/Achievements";
import { Engine } from "../../engine";
export function Achievements(): React.ReactElement {
export function AchievementsDev(): React.ReactElement {
const [playerAchievement, setPlayerAchievements] = useState(Player.achievements.map((m) => m.ID));
function grantAchievement(id: string): void {

@ -14,7 +14,7 @@ import {
} from "@mui/material";
import { AugmentationName } from "@enums";
export function Augmentations(): React.ReactElement {
export function AugmentationsDev(): React.ReactElement {
const [augmentation, setAugmentation] = useState(AugmentationName.Targeting1);
function setAugmentationDropdown(event: SelectChangeEvent): void {

@ -18,13 +18,11 @@ import { Player } from "@player";
import { CityName } from "@enums";
import { Skills as AllSkills } from "../../Bladeburner/Skills";
import { SkillNames } from "../../Bladeburner/data/SkillNames";
import { Bladeburner } from "../../Bladeburner/Bladeburner";
const bigNumber = 1e27;
export function Bladeburner(): React.ReactElement {
if (!Player.bladeburner) return <></>;
const bladeburner = Player.bladeburner;
export function BladeburnerDev({ bladeburner }: { bladeburner: Bladeburner }): React.ReactElement {
// Rank functions
const modifyBladeburnerRank = (modify: number) => (rank: number) => bladeburner.changeRank(Player, rank * modify);
const resetBladeburnerRank = () => {

@ -12,7 +12,7 @@ import MenuItem from "@mui/material/MenuItem";
import { generateContract, generateRandomContract, generateRandomContractOnHome } from "../../CodingContractGenerator";
import { CodingContractTypes } from "../../CodingContracts";
export function CodingContracts(): React.ReactElement {
export function CodingContractsDev(): React.ReactElement {
const [codingcontract, setCodingcontract] = useState("Find Largest Prime Factor");
function setCodingcontractDropdown(event: SelectChangeEvent): void {
setCodingcontract(event.target.value);

@ -15,7 +15,7 @@ import { Adjuster } from "./Adjuster";
const bigNumber = 1e12;
export function Companies(): React.ReactElement {
export function CompaniesDev(): React.ReactElement {
const [company, setCompany] = useState(FactionName.ECorp as string);
function setCompanyDropdown(event: SelectChangeEvent): void {
setCompany(event.target.value);

@ -12,7 +12,7 @@ import { Player } from "@player";
const bigNumber = 1e27;
export function Corporation(): React.ReactElement {
export function CorporationDev(): React.ReactElement {
function addTonsCorporationFunds(): void {
if (Player.corporation) {
Player.corporation.funds = Player.corporation.funds + bigNumber;

@ -9,9 +9,9 @@ import Typography from "@mui/material/Typography";
import { Player } from "@player";
import { Adjuster } from "./Adjuster";
// Update as additional BitNodes get implemented
// TODO: Update as additional BitNodes get implemented
export function Entropy(): React.ReactElement {
export function EntropyDev(): React.ReactElement {
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>

@ -11,7 +11,7 @@ import { Player } from "@player";
const bigNumber = 1e27;
export function Gang(): React.ReactElement {
export function GangDev(): React.ReactElement {
function addTonsGangCycles(): void {
if (Player.gang) {
Player.gang.storedCycles = bigNumber;

@ -12,7 +12,7 @@ import { Player } from "@player";
import MenuItem from "@mui/material/MenuItem";
import { CompletedProgramName } from "@enums";
export function Programs(): React.ReactElement {
export function ProgramsDev(): React.ReactElement {
const [program, setProgram] = useState(CompletedProgramName.bruteSsh);
function setProgramDropdown(event: SelectChangeEvent): void {
setProgram(event.target.value as CompletedProgramName);

@ -13,7 +13,7 @@ import { Upload } from "@mui/icons-material";
import { Button } from "@mui/material";
import { OptionSwitch } from "../../ui/React/OptionSwitch";
export function SaveFile(): React.ReactElement {
export function SaveFileDev(): React.ReactElement {
const importInput = useRef<HTMLInputElement>(null);
const [saveFile, setSaveFile] = useState("");
const [restoreScripts, setRestoreScripts] = useState(true);

@ -12,7 +12,7 @@ import { GetServer, GetAllServers } from "../../Server/AllServers";
import { Server } from "../../Server/Server";
import MenuItem from "@mui/material/MenuItem";
export function Servers(): React.ReactElement {
export function ServersDev(): React.ReactElement {
const [server, setServer] = useState("home");
function setServerDropdown(event: SelectChangeEvent): void {
setServer(event.target.value);

@ -10,7 +10,7 @@ import Typography from "@mui/material/Typography";
import { Player } from "@player";
import { Adjuster } from "./Adjuster";
export function Sleeves(): React.ReactElement {
export function SleevesDev(): React.ReactElement {
function sleeveMaxAllShock(): void {
for (let i = 0; i < Player.sleeves.length; ++i) {
Player.sleeves[i].shock = 100;

@ -13,7 +13,7 @@ import ButtonGroup from "@mui/material/ButtonGroup";
// Update as additional BitNodes get implemented
const validSFN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
export function SourceFiles(): React.ReactElement {
export function SourceFilesDev(): React.ReactElement {
function setSF(sfN: number, sfLvl: number) {
return function () {
if (sfN === 9) {

@ -10,7 +10,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Typography from "@mui/material/Typography";
import { Adjuster } from "./Adjuster";
export function Stanek(): React.ReactElement {
export function StanekDev(): React.ReactElement {
function addCycles(): void {
staneksGift.storedCycles = 1e6;
}

@ -12,7 +12,7 @@ import { Player } from "@player";
const bigNumber = 1e27;
export function Stats(): React.ReactElement {
export function StatsDev(): React.ReactElement {
function modifyExp(stat: string, modifier: number) {
return function (exp: number) {
switch (stat) {

@ -13,7 +13,7 @@ import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { StockMarket as SM } from "../../StockMarket/StockMarket";
import { Stock } from "../../StockMarket/Stock";
export function StockMarket(): React.ReactElement {
export function StockMarketDev(): React.ReactElement {
const [stockPrice, setStockPrice] = useState(0);
const [stockSymbol, setStockSymbol] = useState("");

@ -14,7 +14,7 @@ import { Faction } from "../Faction";
import { getFactionAugmentationsFiltered, joinFaction } from "../FactionHelpers";
import { Factions } from "../Factions";
export const InvitationsSeen: string[] = [];
export const InvitationsSeen = new Set<FactionName>();
const fontSize = "small";
const marginRight = 0.5;
@ -173,8 +173,7 @@ export function FactionsRoot(): React.ReactElement {
const rerender = useRerender(200);
useEffect(() => {
Player.factionInvitations.forEach((faction) => {
if (InvitationsSeen.includes(faction)) return;
InvitationsSeen.push(faction);
InvitationsSeen.add(faction);
});
}, []);

@ -1,31 +1,28 @@
/**
* React Component for the content of the popup before the player confirms the
* ascension of a gang member.
*/
import React, { useState, useEffect } from "react";
import React from "react";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { GangMember } from "../GangMember";
import { formatPreciseMultiplier, formatRespect } from "../../ui/formatNumber";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { Modal } from "../../ui/React/Modal";
import { useGang } from "./Context";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { useRerender } from "../../ui/React/hooks";
interface IProps {
type AscensionModalProps = {
open: boolean;
onClose: () => void;
member: GangMember;
onAscend: () => void;
}
};
export function AscensionModal(props: IProps): React.ReactElement {
/**
* React Component for the content of the popup before the player confirms the
* ascension of a gang member.
*/
export function AscensionModal(props: AscensionModalProps): React.ReactElement {
const gang = useGang();
const setRerender = useState(false)[1];
useEffect(() => {
const id = setInterval(() => setRerender((old) => !old), 1000);
return () => clearInterval(id);
}, []);
useRerender(1000);
function confirm(): void {
props.onAscend();

@ -14,7 +14,7 @@ import { GangMember } from "../GangMember";
import { Settings } from "../../Settings/Settings";
import { MoneyRate } from "../../ui/React/MoneyRate";
import { StatsRow } from "../../ui/React/StatsRow";
import { characterOverviewStyles as useStyles } from "../../ui/React/CharacterOverview";
import { useStyles } from "../../ui/React/CharacterOverview";
interface IProps {
member: GangMember;
@ -34,7 +34,7 @@ export function GangMemberStats(props: IProps): React.ReactElement {
const gang = useGang();
const data = [
[`Money:`, <MoneyRate money={5 * props.member.calculateMoneyGain(gang)} />],
[`Money:`, <MoneyRate key="money" money={5 * props.member.calculateMoneyGain(gang)} />],
[`Respect:`, `${formatRespect(5 * props.member.calculateRespectGain(gang))} / sec`],
[`Wanted Level:`, `${formatWanted(5 * props.member.calculateWantedLevelGain(gang))} / sec`],
[`Total Respect:`, `${formatRespect(props.member.earnedRespect)}`],

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React from "react";
import { ManagementSubpage } from "./ManagementSubpage";
import { TerritorySubpage } from "./TerritorySubpage";
import { EquipmentsSubpage } from "./EquipmentsSubpage";
@ -8,6 +8,8 @@ import { Context } from "./Context";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import { useRerender } from "../../ui/React/hooks";
/** React Component for all the gang stuff. */
export function GangRoot(): React.ReactElement {
const gang = (function () {
@ -20,12 +22,7 @@ export function GangRoot(): React.ReactElement {
setValue(tab);
}
const setRerender = useState(false)[1];
useEffect(() => {
const id = setInterval(() => setRerender((old) => !old), 200);
return () => clearInterval(id);
}, []);
useRerender(200);
return (
<Context.Gang.Provider value={gang}>

@ -1,5 +1,5 @@
import { Button, Container, Paper, Typography } from "@mui/material";
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import { AugmentationName } from "@enums";
import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router";
@ -15,12 +15,12 @@ import { SlashGame } from "./SlashGame";
import { Victory } from "./Victory";
import { WireCuttingGame } from "./WireCuttingGame";
interface IProps {
type GameProps = {
StartingDifficulty: number;
Difficulty: number;
Reward: number;
MaxLevel: number;
}
};
enum Stage {
Countdown = 0,
@ -40,7 +40,7 @@ const minigames = [
WireCuttingGame,
];
export function Game(props: IProps): React.ReactElement {
export function Game(props: GameProps): React.ReactElement {
const [level, setLevel] = useState(1);
const [stage, setStage] = useState(Stage.Countdown);
const [results, setResults] = useState("");
@ -49,32 +49,21 @@ export function Game(props: IProps): React.ReactElement {
id: Math.floor(Math.random() * minigames.length),
});
function nextGameId(): number {
let id = gameIds.lastGames[0];
const ids = [gameIds.lastGames[0], gameIds.lastGames[1], gameIds.id];
while (ids.includes(id)) {
id = Math.floor(Math.random() * minigames.length);
}
return id;
}
const setupNextGame = useCallback(() => {
const nextGameId = () => {
let id = gameIds.lastGames[0];
const ids = [gameIds.lastGames[0], gameIds.lastGames[1], gameIds.id];
while (ids.includes(id)) {
id = Math.floor(Math.random() * minigames.length);
}
return id;
};
function setupNextGame(): void {
setGameIds({
lastGames: [gameIds.lastGames[1], gameIds.id],
id: nextGameId(),
});
}
function success(): void {
pushResult(true);
if (level === props.MaxLevel) {
setStage(Stage.Sell);
} else {
setStage(Stage.Countdown);
setLevel(level + 1);
}
setupNextGame();
}
}, [gameIds]);
function pushResult(win: boolean): void {
setResults((old) => {
@ -85,20 +74,34 @@ export function Game(props: IProps): React.ReactElement {
});
}
function failure(options?: { automated: boolean }): void {
setStage(Stage.Countdown);
pushResult(false);
// Kill the player immediately if they use automation, so
// it's clear they're not meant to
const damage = options?.automated
? Player.hp.current
: props.StartingDifficulty * 3 * (Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 0.5 : 1);
if (Player.takeDamage(damage)) {
Router.toPage(Page.City);
return;
const onSuccess = useCallback(() => {
pushResult(true);
if (level === props.MaxLevel) {
setStage(Stage.Sell);
} else {
setStage(Stage.Countdown);
setLevel(level + 1);
}
setupNextGame();
}
}, [level, props.MaxLevel, setupNextGame]);
const onFailure = useCallback(
(options?: { automated: boolean }) => {
setStage(Stage.Countdown);
pushResult(false);
// Kill the player immediately if they use automation, so
// it's clear they're not meant to
const damage = options?.automated
? Player.hp.current
: props.StartingDifficulty * 3 * (Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 0.5 : 1);
if (Player.takeDamage(damage)) {
Router.toPage(Page.City);
return;
}
setupNextGame();
},
[props.StartingDifficulty, setupNextGame],
);
function cancel(): void {
Router.toPage(Page.City);
@ -112,7 +115,9 @@ export function Game(props: IProps): React.ReactElement {
break;
case Stage.Minigame: {
const MiniGame = minigames[gameIds.id];
stageComponent = <MiniGame onSuccess={success} onFailure={failure} difficulty={props.Difficulty + level / 50} />;
stageComponent = (
<MiniGame onSuccess={onSuccess} onFailure={onFailure} difficulty={props.Difficulty + level / 50} />
);
break;
}
case Stage.Sell:

@ -4,36 +4,41 @@ import { AugmentationName } from "@enums";
import { Player } from "@player";
import { ProgressBar } from "../../ui/React/Progress";
interface IProps {
type GameTimerProps = {
millis: number;
onExpire: () => void;
noPaper?: boolean;
ignoreAugment_WKSharmonizer?: boolean;
}
};
export function GameTimer(props: IProps): React.ReactElement {
export function GameTimer({
millis,
onExpire,
noPaper,
ignoreAugment_WKSharmonizer,
}: GameTimerProps): React.ReactElement {
const [v, setV] = useState(100);
const totalMillis =
(!props.ignoreAugment_WKSharmonizer && Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 1.3 : 1) *
props.millis;
(!ignoreAugment_WKSharmonizer && Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 1.3 : 1) * millis;
const tick = 200;
useEffect(() => {
const intervalId = setInterval(() => {
setV((old) => {
if (old <= 0) props.onExpire();
if (old <= 0) onExpire();
return old - (tick / totalMillis) * 100;
});
}, tick);
return () => {
clearInterval(intervalId);
};
}, []);
}, [onExpire, totalMillis]);
// https://stackoverflow.com/questions/55593367/disable-material-uis-linearprogress-animation
// TODO(hydroflame): there's like a bug where it triggers the end before the
// bar physically reaches the end
return props.noPaper ? (
return noPaper ? (
<ProgressBar variant="determinate" value={v} color="primary" />
) : (
<Paper sx={{ p: 1, mb: 1 }}>

@ -1,5 +1,5 @@
import { Box, Paper, Typography } from "@mui/material";
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { AugmentationName } from "@enums";
import { Player } from "@player";
import { KEY } from "../../utils/helpers/keyCodes";
@ -25,48 +25,52 @@ const difficulties: {
Impossible: { window: 150 },
};
export function SlashGame(props: IMinigameProps): React.ReactElement {
export function SlashGame({ difficulty: _difficulty, onSuccess, onFailure }: IMinigameProps): React.ReactElement {
const difficulty: Difficulty = { window: 0 };
interpolate(difficulties, props.difficulty, difficulty);
interpolate(difficulties, _difficulty, difficulty);
const [phase, setPhase] = useState(0);
function press(this: Document, event: KeyboardEvent): void {
event.preventDefault();
if (event.key !== KEY.SPACE) return;
if (phase !== 1) {
props.onFailure();
onFailure();
} else {
props.onSuccess();
onSuccess();
}
}
const hasAugment = Player.hasAugmentation(AugmentationName.MightOfAres, true);
const guardingTime = Math.random() * 3250 + 1500 - (250 + difficulty.window);
const preparingTime = difficulty.window;
const attackingTime = 250;
const guardingTimeRef = useRef(Math.random() * 3250 + 1500 - (250 + difficulty.window));
useEffect(() => {
const preparingTime = difficulty.window;
const attackingTime = 250;
let id = window.setTimeout(() => {
setPhase(1);
id = window.setTimeout(() => {
setPhase(2);
id = window.setTimeout(() => props.onFailure(), attackingTime);
id = window.setTimeout(() => onFailure(), attackingTime);
}, preparingTime);
}, guardingTime);
}, guardingTimeRef.current);
return () => {
clearInterval(id);
};
}, []);
}, [difficulty.window, onFailure]);
const hasAugment = Player.hasAugmentation(AugmentationName.MightOfAres, true);
return (
<>
<GameTimer millis={5000} onExpire={props.onFailure} />
<GameTimer millis={5000} onExpire={onFailure} />
<Paper sx={{ display: "grid", justifyItems: "center" }}>
<Typography variant="h4">Attack when his guard is down!</Typography>
{hasAugment ? (
<Box sx={{ my: 1 }}>
<Typography variant="h5">Guard will drop in...</Typography>
<GameTimer millis={guardingTime} onExpire={() => null} ignoreAugment_WKSharmonizer noPaper />
<GameTimer millis={guardingTimeRef.current} onExpire={() => null} ignoreAugment_WKSharmonizer noPaper />
</Box>
) : (
<></>
@ -75,7 +79,7 @@ export function SlashGame(props: IMinigameProps): React.ReactElement {
{phase === 0 && <Typography variant="h4">Guarding ...</Typography>}
{phase === 1 && <Typography variant="h4">Preparing?</Typography>}
{phase === 2 && <Typography variant="h4">ATTACKING!</Typography>}
<KeyHandler onKeyDown={press} onFailure={props.onFailure} />
<KeyHandler onKeyDown={press} onFailure={onFailure} />
</Paper>
</>
);

@ -1,5 +1,6 @@
import React, { useEffect, useState, useRef } from "react";
import { Box, Paper, Typography } from "@mui/material";
import React, { useEffect, useState } from "react";
import { AugmentationName } from "@enums";
import { Player } from "@player";
import { Settings } from "../../Settings/Settings";
@ -51,49 +52,51 @@ interface Question {
shouldCut: (wire: Wire, index: number) => boolean;
}
export function WireCuttingGame(props: IMinigameProps): React.ReactElement {
export function WireCuttingGame({ onSuccess, onFailure, ...otherProps }: IMinigameProps): React.ReactElement {
const difficulty: Difficulty = {
timer: 0,
wiresmin: 0,
wiresmax: 0,
rules: 0,
};
interpolate(difficulties, props.difficulty, difficulty);
interpolate(difficulties, otherProps.difficulty, difficulty);
const timer = difficulty.timer;
const [wires] = useState(generateWires(difficulty));
const [cutWires, setCutWires] = useState(new Array(wires.length).fill(false));
const [questions] = useState(generateQuestion(wires, difficulty));
const wiresRef = useRef(generateWires(difficulty));
const questionsRef = useRef(generateQuestion(wiresRef.current, difficulty));
const [cutWires, setCutWires] = useState(new Array(wiresRef.current.length).fill(false));
const hasAugment = Player.hasAugmentation(AugmentationName.KnowledgeOfApollo, true);
function checkWire(wireNum: number): boolean {
return questions.some((q) => q.shouldCut(wires[wireNum - 1], wireNum - 1));
}
// TODO: refactor, move the code from this effect to a `press` function
useEffect(() => {
// check if we won
const wiresToBeCut = [];
for (let j = 0; j < wires.length; j++) {
for (let j = 0; j < wiresRef.current.length; j++) {
let shouldBeCut = false;
for (let i = 0; i < questions.length; i++) {
shouldBeCut = shouldBeCut || questions[i].shouldCut(wires[j], j);
for (let i = 0; i < questionsRef.current.length; i++) {
shouldBeCut = shouldBeCut || questionsRef.current[i].shouldCut(wiresRef.current[j], j);
}
wiresToBeCut.push(shouldBeCut);
}
if (wiresToBeCut.every((b, i) => b === cutWires[i])) {
props.onSuccess();
onSuccess();
}
}, [cutWires]);
}, [cutWires, onSuccess]);
function checkWire(wireNum: number): boolean {
return questionsRef.current.some((q) => q.shouldCut(wiresRef.current[wireNum - 1], wireNum - 1));
}
function press(this: Document, event: KeyboardEvent): void {
event.preventDefault();
const wireNum = parseInt(event.key);
if (wireNum < 1 || wireNum > wires.length || isNaN(wireNum)) return;
if (wireNum < 1 || wireNum > wiresRef.current.length || isNaN(wireNum)) return;
setCutWires((old) => {
const next = [...old];
next[wireNum - 1] = true;
if (!checkWire(wireNum)) {
props.onFailure();
onFailure();
}
return next;
@ -102,23 +105,23 @@ export function WireCuttingGame(props: IMinigameProps): React.ReactElement {
return (
<>
<GameTimer millis={timer} onExpire={props.onFailure} />
<GameTimer millis={timer} onExpire={onFailure} />
<Paper sx={{ display: "grid", justifyItems: "center", pb: 1 }}>
<Typography variant="h4" sx={{ width: "75%", textAlign: "center" }}>
Cut the wires with the following properties! (keyboard 1 to 9)
</Typography>
{questions.map((question, i) => (
{questionsRef.current.map((question, i) => (
<Typography key={i}>{question.toString()}</Typography>
))}
<Box
sx={{
display: "grid",
gridTemplateColumns: `repeat(${wires.length}, 1fr)`,
gridTemplateColumns: `repeat(${wiresRef.current.length}, 1fr)`,
columnGap: 3,
justifyItems: "center",
}}
>
{new Array(wires.length).fill(0).map((_, i) => {
{Array.from({ length: wiresRef.current.length }).map((_, i) => {
const isCorrectWire = checkWire(i + 1);
const color = hasAugment && !isCorrectWire ? Settings.theme.disabled : Settings.theme.primary;
return (
@ -129,7 +132,7 @@ export function WireCuttingGame(props: IMinigameProps): React.ReactElement {
})}
{new Array(8).fill(0).map((_, i) => (
<React.Fragment key={i}>
{wires.map((wire, j) => {
{wiresRef.current.map((wire, j) => {
if ((i === 3 || i === 4) && cutWires[j]) {
return <Typography key={j}></Typography>;
}
@ -145,7 +148,7 @@ export function WireCuttingGame(props: IMinigameProps): React.ReactElement {
</React.Fragment>
))}
</Box>
<KeyHandler onKeyDown={press} onFailure={props.onFailure} />
<KeyHandler onKeyDown={press} onFailure={onFailure} />
</Paper>
</>
);

@ -31,7 +31,7 @@ export function SlumsLocation(): React.ReactElement {
return (
<Box sx={{ display: "grid", width: "fit-content" }}>
{crimes.map((crime) => (
<Tooltip title={crime.tooltipText}>
<Tooltip key={crime.workName} title={crime.tooltipText}>
<Button onClick={(e) => doCrime(e, crime)}>
{crime.type} ({formatPercent(crime.successRate(Player))} chance of success)
</Button>

@ -801,5 +801,5 @@ let customElementKey = 0;
* so the game won't crash and the user gets sensible messages.
*/
export function wrapUserNode(value: unknown) {
return <CustomBoundary key={`PlayerContent${customElementKey++}`} children={value as React.ReactNode} />;
return <CustomBoundary key={`PlayerContent${customElementKey++}`}>{value}</CustomBoundary>;
}

@ -52,7 +52,7 @@ const AugPreReqsChecklist = (props: IProps): React.ReactElement => {
<b>Pre-Requisites:</b>
<br />
{aug.prereqs.map((preAug) => (
<span style={{ display: "flex", alignItems: "center" }}>
<span key={preAug} style={{ display: "flex", alignItems: "center" }}>
{Player.hasAugmentation(preAug) ? <CheckBox sx={{ mr: 1 }} /> : <CheckBoxOutlineBlank sx={{ mr: 1 }} />}
{preAug}
</span>

@ -13,7 +13,7 @@ import {
} from "../../../ui/formatNumber";
import { Settings } from "../../../Settings/Settings";
import { StatsRow } from "../../../ui/React/StatsRow";
import { characterOverviewStyles as useStyles } from "../../../ui/React/CharacterOverview";
import { useStyles } from "../../../ui/React/CharacterOverview";
import { Money } from "../../../ui/React/Money";
import { MoneyRate } from "../../../ui/React/MoneyRate";
import { ReputationRate } from "../../../ui/React/ReputationRate";
@ -106,7 +106,7 @@ export function EarningsElement(props: IProps): React.ReactElement {
if (isSleeveCrimeWork(props.sleeve.currentWork)) {
const gains = props.sleeve.currentWork.getExp(props.sleeve);
data = [
[`Money:`, <Money money={gains.money} />],
[`Money:`, <Money key="money" money={gains.money} />],
[`Hacking Exp:`, `${formatExp(gains.hackExp)}`],
[`Strength Exp:`, `${formatExp(gains.strExp)}`],
[`Defense Exp:`, `${formatExp(gains.defExp)}`],
@ -118,7 +118,7 @@ export function EarningsElement(props: IProps): React.ReactElement {
if (isSleeveClassWork(props.sleeve.currentWork)) {
const rates = props.sleeve.currentWork.calculateRates(props.sleeve);
data = [
[`Money:`, <MoneyRate money={CYCLES_PER_SEC * rates.money} />],
[`Money:`, <MoneyRate key="money-rate" money={CYCLES_PER_SEC * rates.money} />],
[`Hacking Exp:`, `${formatExp(CYCLES_PER_SEC * rates.hackExp)} / sec`],
[`Strength Exp:`, `${formatExp(CYCLES_PER_SEC * rates.strExp)} / sec`],
[`Defense Exp:`, `${formatExp(CYCLES_PER_SEC * rates.defExp)} / sec`],
@ -137,21 +137,21 @@ export function EarningsElement(props: IProps): React.ReactElement {
[`Dexterity Exp:`, `${formatExp(CYCLES_PER_SEC * rates.dexExp)} / sec`],
[`Agility Exp:`, `${formatExp(CYCLES_PER_SEC * rates.agiExp)} / sec`],
[`Charisma Exp:`, `${formatExp(CYCLES_PER_SEC * rates.chaExp)} / sec`],
[`Reputation:`, <ReputationRate reputation={CYCLES_PER_SEC * repGain} />],
[`Reputation:`, <ReputationRate key="reputation-rate" reputation={CYCLES_PER_SEC * repGain} />],
];
}
if (isSleeveCompanyWork(props.sleeve.currentWork)) {
const rates = props.sleeve.currentWork.getGainRates(props.sleeve);
data = [
[`Money:`, <MoneyRate money={CYCLES_PER_SEC * rates.money} />],
[`Money:`, <MoneyRate key="money-rate" money={CYCLES_PER_SEC * rates.money} />],
[`Hacking Exp:`, `${formatExp(CYCLES_PER_SEC * rates.hackExp)} / sec`],
[`Strength Exp:`, `${formatExp(CYCLES_PER_SEC * rates.strExp)} / sec`],
[`Defense Exp:`, `${formatExp(CYCLES_PER_SEC * rates.defExp)} / sec`],
[`Dexterity Exp:`, `${formatExp(CYCLES_PER_SEC * rates.dexExp)} / sec`],
[`Agility Exp:`, `${formatExp(CYCLES_PER_SEC * rates.agiExp)} / sec`],
[`Charisma Exp:`, `${formatExp(CYCLES_PER_SEC * rates.chaExp)} / sec`],
[`Reputation:`, <ReputationRate reputation={CYCLES_PER_SEC * rates.reputation} />],
[`Reputation:`, <ReputationRate key="reputation-rate" reputation={CYCLES_PER_SEC * rates.reputation} />],
];
}

@ -152,8 +152,8 @@ export function prestigeAugmentation(): void {
staneksGift.prestigeAugmentation();
resetPidCounter();
ProgramsSeen.splice(0, ProgramsSeen.length);
InvitationsSeen.splice(0, InvitationsSeen.length);
ProgramsSeen.clear();
InvitationsSeen.clear();
}
// Prestige by destroying Bit Node and gaining a Source File

@ -13,7 +13,7 @@ import { Programs } from "../Programs";
import { CreateProgramWork, isCreateProgramWork } from "../../Work/CreateProgramWork";
import { useRerender } from "../../ui/React/hooks";
export const ProgramsSeen: string[] = [];
export const ProgramsSeen = new Set<string>();
export function ProgramsRoot(): React.ReactElement {
useRerender(200);
@ -35,9 +35,9 @@ export function ProgramsRoot(): React.ReactElement {
useEffect(() => {
programs.forEach((p) => {
if (ProgramsSeen.includes(p.name)) return;
ProgramsSeen.push(p.name);
ProgramsSeen.add(p.name);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getHackingLevelRemaining = (lvl: number): number => {

@ -46,6 +46,9 @@ export function Editor({ beforeMount, onMount, onChange }: EditorProps) {
editorRef.current?.getModel()?.dispose();
editorRef.current?.dispose();
};
// this eslint ignore instruction can potentially cause unobvious bugs
// (e.g. if `onChange` starts using a prop or state in parent component).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div ref={containerDiv} style={{ height: "1px", width: "100%", flexGrow: 1 }} />;

@ -77,14 +77,6 @@ function Root(props: IProps): React.ReactElement {
currentScript = openScripts[0] ?? null;
}
useEffect(() => {
if (currentScript !== null) {
const tabIndex = currentTabIndex();
if (typeof tabIndex === "number") onTabClick(tabIndex);
parseCode(currentScript.code);
}
}, []);
useEffect(() => {
function keydown(event: KeyboardEvent): void {
if (Settings.DisableHotkeys) return;
@ -145,10 +137,10 @@ function Root(props: IProps): React.ReactElement {
finishUpdatingRAM();
}, 300);
function parseCode(newCode: string) {
const parseCode = (newCode: string) => {
startUpdatingRAM();
debouncedCodeParsing(newCode);
}
};
// How to load function definition in monaco
// https://github.com/Microsoft/monaco-editor/issues/1415
@ -444,6 +436,16 @@ function Root(props: IProps): React.ReactElement {
onOpenPreviousTab,
});
useEffect(() => {
if (currentScript !== null) {
const tabIndex = currentTabIndex();
if (typeof tabIndex === "number") onTabClick(tabIndex);
parseCode(currentScript.code);
}
// disable eslint because we want to run this only once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<div

@ -23,6 +23,9 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
const vimStatusRef = useRef<HTMLElement>(null);
const actionsRef = useRef({ save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab });
actionsRef.current = { save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab };
useEffect(() => {
// setup monaco-vim
if (vim && editor && !vimEditor) {
@ -31,14 +34,14 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
setVimEditor(MonacoVim.initVimMode(editor, vimStatusRef.current));
MonacoVim.VimMode.Vim.defineEx("write", "w", function () {
// your own implementation on what you want to do when :w is pressed
onSave();
actionsRef.current.save();
});
MonacoVim.VimMode.Vim.defineEx("quit", "q", function () {
Router.toPage(Page.Terminal);
});
const saveNQuit = (): void => {
onSave();
actionsRef.current.save();
Router.toPage(Page.Terminal);
};
// "wqriteandquit" & "xriteandquit" are not typos, prefix must be found in full string
@ -48,10 +51,10 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
// Setup "go to next tab" and "go to previous tab". This is a little more involved
// since these aren't Ex commands (they run in normal mode, not after typing `:`)
MonacoVim.VimMode.Vim.defineAction("nextTabs", function (_cm: any, { repeat = 1 }: { repeat?: number }) {
onOpenNextTab(repeat);
actionsRef.current.openNextTab(repeat);
});
MonacoVim.VimMode.Vim.defineAction("prevTabs", function (_cm: any, { repeat = 1 }: { repeat?: number }) {
onOpenPreviousTab(repeat);
actionsRef.current.openPreviousTab(repeat);
});
MonacoVim.VimMode.Vim.mapCommand("gt", "action", "nextTabs", {}, { context: "normal" });
MonacoVim.VimMode.Vim.mapCommand("gT", "action", "prevTabs", {}, { context: "normal" });

@ -11,7 +11,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { SidebarItem, ICreateProps as IItemProps } from "./SidebarItem";
import type { Page } from "../../ui/Router";
interface IProps {
type SidebarAccordionProps = {
key_: string;
page: Page;
clickPage: (page: Page) => void;
@ -20,7 +20,7 @@ interface IProps {
icon: React.ReactElement["type"];
sidebarOpen: boolean;
classes: any;
}
};
// We can't useCallback for this, because in the items map it would be
// called a changing number of times, and hooks can't be called in loops. So
@ -42,9 +42,18 @@ function getClickFn(toWrap: (page: Page) => void, page: Page) {
}
// This can't be usefully memoized, because props.items is a new array every time.
export function SidebarAccordion(props: IProps): React.ReactElement {
export function SidebarAccordion({
classes,
icon: Icon,
sidebarOpen,
key_,
items,
page,
clickPage,
flash,
}: SidebarAccordionProps): React.ReactElement {
const [open, setOpen] = useState(true);
const li_classes = useMemo(() => ({ root: props.classes.listitem }), [props.classes.listitem]);
const li_classes = useMemo(() => ({ root: classes.listitem }), [classes.listitem]);
// Explicitily useMemo() to save rerendering deep chunks of this tree.
// memo() can't be (easily) used on components like <List>, because the
@ -55,18 +64,18 @@ export function SidebarAccordion(props: IProps): React.ReactElement {
() => (
<ListItem classes={li_classes} button onClick={() => setOpen((open) => !open)}>
<ListItemIcon>
<Tooltip title={!props.sidebarOpen ? props.key_ : ""}>
<props.icon color={"primary"} />
<Tooltip title={!sidebarOpen ? key_ : ""}>
<Icon color={"primary"} />
</Tooltip>
</ListItemIcon>
<ListItemText primary={<Typography>{props.key_}</Typography>} />
<ListItemText primary={<Typography>{key_}</Typography>} />
{open ? <ExpandLessIcon color="primary" /> : <ExpandMoreIcon color="primary" />}
</ListItem>
),
[li_classes, props.sidebarOpen, props.key_, open, props.icon],
[li_classes, sidebarOpen, key_, open, Icon],
)}
<Collapse in={open} timeout="auto" unmountOnExit>
{props.items.map((x) => {
{items.map((x) => {
if (typeof x !== "object") return null;
const { key_, icon, count, active } = x;
return (
@ -75,11 +84,11 @@ export function SidebarAccordion(props: IProps): React.ReactElement {
key_={key_}
icon={icon}
count={count}
active={active ?? props.page === key_}
clickFn={getClickFn(props.clickPage, key_)}
flash={props.flash === key_}
classes={props.classes}
sidebarOpen={props.sidebarOpen}
active={active ?? page === key_}
clickFn={getClickFn(clickPage, key_)}
flash={flash === key_}
classes={classes}
sidebarOpen={sidebarOpen}
/>
);
})}

@ -15,14 +15,14 @@ export interface ICreateProps {
active?: boolean;
}
export interface IProps extends ICreateProps {
export interface SidebarItemProps extends ICreateProps {
clickFn: () => void;
flash: boolean;
classes: any;
sidebarOpen: boolean;
}
export const SidebarItem = memo(function (props: IProps): React.ReactElement {
export const SidebarItem = memo(function SidebarItem(props: SidebarItemProps): React.ReactElement {
const color = props.flash ? "error" : props.active ? "primary" : "secondary";
return (
<ListItem
@ -40,7 +40,7 @@ export const SidebarItem = memo(function (props: IProps): React.ReactElement {
</Badge>
</ListItemIcon>
<ListItemText>
<Typography color={color} children={props.key_} />
<Typography color={color}>{props.key_}</Typography>
</ListItemText>
</ListItem>
);

@ -56,9 +56,12 @@ import { hash } from "../../hash/hash";
import { Locations } from "../../Locations/Locations";
import { useRerender } from "../../ui/React/hooks";
const RotatedDoubleArrowIcon = React.forwardRef((props: { color: "primary" | "secondary" | "error" }, __ref) => (
<DoubleArrowIcon color={props.color} style={{ transform: "rotate(-90deg)" }} />
));
const RotatedDoubleArrowIcon = React.forwardRef(function RotatedDoubleArrowIcon(
props: { color: "primary" | "secondary" | "error" },
__ref: React.ForwardedRef<SVGSVGElement>,
) {
return <DoubleArrowIcon color={props.color} style={{ transform: "rotate(-90deg)" }} ref={__ref} />;
});
const openedMixin = (theme: Theme): CSSObject => ({
width: theme.spacing(31),
@ -131,8 +134,8 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
}
const augmentationCount = Player.queuedAugmentations.length;
const invitationsCount = Player.factionInvitations.filter((f) => !InvitationsSeen.includes(f)).length;
const programCount = getAvailableCreatePrograms().length - ProgramsSeen.length;
const invitationsCount = Player.factionInvitations.filter((f) => !InvitationsSeen.has(f)).length;
const programCount = getAvailableCreatePrograms().length - ProgramsSeen.size;
const canOpenFactions =
Player.factionInvitations.length > 0 ||
@ -156,6 +159,24 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
const canBladeburner = !!Player.bladeburner;
const canStaneksGift = Player.augmentations.some((aug) => aug.name === AugmentationName.StaneksGift1);
const clickPage = useCallback(
(page: Page) => {
if (page === Page.Job) {
Router.toPage(page, { location: Locations[Object.keys(Player.jobs)[0]] });
} else if (page == Page.ScriptEditor) {
Router.toPage(page, {});
} else if (isSimplePage(page)) {
Router.toPage(page);
} else {
throw new Error("Can't handle click on Page " + page);
}
if (flash === page) {
iTutorialNextStep();
}
},
[flash],
);
useEffect(() => {
// Shortcuts to navigate through the game
// Alt-t - Terminal
@ -230,25 +251,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
document.addEventListener("keydown", handleShortcuts);
return () => document.removeEventListener("keydown", handleShortcuts);
}, []);
const clickPage = useCallback(
(page: Page) => {
if (page === Page.Job) {
Router.toPage(page, { location: Locations[Object.keys(Player.jobs)[0]] });
} else if (page == Page.ScriptEditor) {
Router.toPage(page, {});
} else if (isSimplePage(page)) {
Router.toPage(page);
} else {
throw new Error("Can't handle click on Page " + page);
}
if (flash === page) {
iTutorialNextStep();
}
},
[flash],
);
}, [canJob, clickPage, props.page]);
const classes = useStyles();
const [open, setOpen] = useState(Settings.IsSidebarOpened);
@ -280,7 +283,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
/>
</ListItem>
),
[li_classes, open],
[ChevronOpenClose, li_classes],
)}
<Divider />
<List>

@ -44,10 +44,6 @@ export function TerminalRoot(): React.ReactElement {
const rerender = useRerender();
const [key, setKey] = useState(0);
function clear(): void {
setKey((key) => key + 1);
}
useEffect(() => {
const debounced = _.debounce(async () => rerender(), 25, { maxWait: 50 });
const unsubscribe = TerminalEvents.subscribe(debounced);
@ -55,9 +51,10 @@ export function TerminalRoot(): React.ReactElement {
debounced.cancel();
unsubscribe();
};
}, []);
}, [rerender]);
useEffect(() => {
const clear = () => setKey((key) => key + 1);
const debounced = _.debounce(async () => clear(), 25, { maxWait: 50 });
const unsubscribe = TerminalClearEvents.subscribe(debounced);
return () => {

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { Modal } from "../../ui/React/Modal";
import Button from "@mui/material/Button";
@ -23,28 +23,18 @@ interface IProps {
interface FontFamilyProps {
value: React.CSSProperties["fontFamily"];
onChange: (newValue: React.CSSProperties["fontFamily"], error?: string) => void;
refreshId: number;
}
function FontFamilyField({ value, onChange, refreshId }: FontFamilyProps): React.ReactElement {
function FontFamilyField({ value, onChange }: FontFamilyProps): React.ReactElement {
const [errorText, setErrorText] = useState<string | undefined>();
const [fontFamily, setFontFamily] = useState<React.CSSProperties["fontFamily"]>(value);
function update(newValue: React.CSSProperties["fontFamily"]): void {
const update = (newValue: React.CSSProperties["fontFamily"]) => {
const errorText = newValue ? "" : "Must have a value";
setFontFamily(newValue);
if (!newValue) {
setErrorText("Must have a value");
} else {
setErrorText("");
}
}
function onTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
update(event.target.value);
}
useEffect(() => onChange(fontFamily, errorText), [fontFamily]);
useEffect(() => update(value), [refreshId]);
setErrorText(errorText);
onChange(newValue, errorText);
};
return (
<TextField
@ -53,7 +43,7 @@ function FontFamilyField({ value, onChange, refreshId }: FontFamilyProps): React
error={!!errorText}
value={fontFamily}
helperText={errorText}
onChange={onTextChange}
onChange={(event) => update(event.target.value)}
fullWidth
/>
);
@ -62,30 +52,19 @@ function FontFamilyField({ value, onChange, refreshId }: FontFamilyProps): React
interface LineHeightProps {
value: React.CSSProperties["lineHeight"];
onChange: (newValue: React.CSSProperties["lineHeight"], error?: string) => void;
refreshId: number;
}
function LineHeightField({ value, onChange, refreshId }: LineHeightProps): React.ReactElement {
function LineHeightField({ value, onChange }: LineHeightProps): React.ReactElement {
const [errorText, setErrorText] = useState<string | undefined>();
const [lineHeight, setLineHeight] = useState<React.CSSProperties["lineHeight"]>(value);
function update(newValue: React.CSSProperties["lineHeight"]): void {
const update = (newValue: React.CSSProperties["lineHeight"]) => {
const errorText = !newValue ? "Must have a value" : isNaN(Number(newValue)) ? "Must be a number" : "";
setLineHeight(newValue);
if (!newValue) {
setErrorText("Must have a value");
} else if (isNaN(Number(newValue))) {
setErrorText("Must be a number");
} else {
setErrorText("");
}
}
function onTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
update(event.target.value);
}
useEffect(() => onChange(lineHeight, errorText), [lineHeight]);
useEffect(() => update(value), [refreshId]);
setErrorText(errorText);
onChange(newValue, errorText);
};
return (
<TextField
@ -94,13 +73,12 @@ function LineHeightField({ value, onChange, refreshId }: LineHeightProps): React
error={!!errorText}
value={lineHeight}
helperText={errorText}
onChange={onTextChange}
onChange={(event) => update(event.target.value)}
/>
);
}
export function StyleEditorModal(props: IProps): React.ReactElement {
const [refreshId, setRefreshId] = useState<number>(0);
const [error, setError] = useState<string | undefined>();
const [customStyle, setCustomStyle] = useState<IStyleSettings>({
...Settings.styles,
@ -119,7 +97,6 @@ export function StyleEditorModal(props: IProps): React.ReactElement {
const styles = { ...defaultStyles };
setCustomStyle(styles);
persistToSettings(styles);
setRefreshId(refreshId + 1);
}
function update(styles: IStyleSettings, errorMessage?: string): void {
@ -139,13 +116,11 @@ export function StyleEditorModal(props: IProps): React.ReactElement {
<Paper sx={{ p: 2, my: 2 }}>
<FontFamilyField
value={customStyle.fontFamily}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, fontFamily: value ?? "" }, error)}
/>
<br />
<LineHeightField
value={customStyle.lineHeight}
refreshId={refreshId}
onChange={(value, error) => update({ ...customStyle, lineHeight: Number(value) ?? 0 }, error)}
/>
<br />

@ -106,60 +106,60 @@ interface IMoneyModalProps {
function MoneyModal({ open, onClose }: IMoneyModalProps): React.ReactElement {
function convertMoneySourceTrackerToString(src: MoneySourceTracker): React.ReactElement {
const parts: [string, JSX.Element][] = [[`Total:`, <Money money={src.total} />]];
const parts: [string, JSX.Element][] = [[`Total:`, <Money key="total" money={src.total} />]];
if (src.augmentations) {
parts.push([`Augmentations:`, <Money money={src.augmentations} />]);
parts.push([`Augmentations:`, <Money key="aug" money={src.augmentations} />]);
}
if (src.bladeburner) {
parts.push([`Bladeburner:`, <Money money={src.bladeburner} />]);
parts.push([`Bladeburner:`, <Money key="blade" money={src.bladeburner} />]);
}
if (src.casino) {
parts.push([`Casino:`, <Money money={src.casino} />]);
parts.push([`Casino:`, <Money key="casino" money={src.casino} />]);
}
if (src.codingcontract) {
parts.push([`Coding Contracts:`, <Money money={src.codingcontract} />]);
parts.push([`Coding Contracts:`, <Money key="coding-contract" money={src.codingcontract} />]);
}
if (src.work) {
parts.push([`Company Work:`, <Money money={src.work} />]);
parts.push([`Company Work:`, <Money key="company-work" money={src.work} />]);
}
if (src.class) {
parts.push([`Class:`, <Money money={src.class} />]);
parts.push([`Class:`, <Money key="class" money={src.class} />]);
}
if (src.corporation) {
parts.push([`Corporation:`, <Money money={src.corporation} />]);
parts.push([`Corporation:`, <Money key="corp" money={src.corporation} />]);
}
if (src.crime) {
parts.push([`Crimes:`, <Money money={src.crime} />]);
parts.push([`Crimes:`, <Money key="crime" money={src.crime} />]);
}
if (src.gang) {
parts.push([`Gang:`, <Money money={src.gang} />]);
parts.push([`Gang:`, <Money key="gang" money={src.gang} />]);
}
if (src.hacking) {
parts.push([`Hacking:`, <Money money={src.hacking} />]);
parts.push([`Hacking:`, <Money key="hacking" money={src.hacking} />]);
}
if (src.hacknet) {
parts.push([`Hacknet Nodes:`, <Money money={src.hacknet} />]);
parts.push([`Hacknet Nodes:`, <Money key="hacknet" money={src.hacknet} />]);
}
if (src.hacknet_expenses) {
parts.push([`Hacknet Nodes Expenses:`, <Money money={src.hacknet_expenses} />]);
parts.push([`Hacknet Nodes Expenses:`, <Money key="hacknet-expenses" money={src.hacknet_expenses} />]);
}
if (src.hospitalization) {
parts.push([`Hospitalization:`, <Money money={src.hospitalization} />]);
parts.push([`Hospitalization:`, <Money key="hospital" money={src.hospitalization} />]);
}
if (src.infiltration) {
parts.push([`Infiltration:`, <Money money={src.infiltration} />]);
parts.push([`Infiltration:`, <Money key="infiltration" money={src.infiltration} />]);
}
if (src.servers) {
parts.push([`Servers:`, <Money money={src.servers} />]);
parts.push([`Servers:`, <Money key="servers" money={src.servers} />]);
}
if (src.stock) {
parts.push([`Stock Market:`, <Money money={src.stock} />]);
parts.push([`Stock Market:`, <Money key="market" money={src.stock} />]);
}
if (src.sleeves) {
parts.push([`Sleeves:`, <Money money={src.sleeves} />]);
parts.push([`Sleeves:`, <Money key="sleeves" money={src.sleeves} />]);
}
if (src.other) {
parts.push([`Other:`, <Money money={src.other} />]);
parts.push([`Other:`, <Money key="other" money={src.other} />]);
}
return <StatsTable rows={parts} wide />;

@ -32,7 +32,9 @@ export function ButtonWithTooltip({
return (
<Tooltip {...tooltipProps} title={tooltipText}>
<span>
<Button {...buttonProps} disabled={disabled} onClick={onClick} children={children} />
<Button {...buttonProps} disabled={disabled} onClick={onClick}>
{children}
</Button>
</span>
</Tooltip>
);

@ -5,27 +5,26 @@ import { RecoveryRoot } from "./React/RecoveryRoot";
import { Page } from "./Router";
import { Router } from "./GameRoot";
interface IProps {
type ErrorBoundaryProps = {
softReset: () => void;
}
children: React.ReactNode;
};
interface IState {
type ErrorBoundaryState = {
error?: Error;
errorInfo?: React.ErrorInfo;
page?: Page;
hasError: boolean;
}
};
export class ErrorBoundary extends React.Component<IProps, IState> {
state: IState;
constructor(props: IProps) {
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false } as IState;
this.state = { hasError: false };
}
reset(): void {
this.setState({ hasError: false } as IState);
this.setState({ hasError: false });
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
@ -35,6 +34,7 @@ export class ErrorBoundary extends React.Component<IProps, IState> {
});
console.error(error, errorInfo);
}
render(): React.ReactNode {
if (this.state.hasError) {
let errorData: IErrorData | undefined;
@ -51,7 +51,8 @@ export class ErrorBoundary extends React.Component<IProps, IState> {
}
return this.props.children;
}
static getDerivedStateFromError(error: Error): IState {
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
}

@ -141,7 +141,7 @@ export function GameRoot(): React.ReactElement {
useEffect(() => {
return ITutorialEvents.subscribe(rerender);
}, []);
}, [rerender]);
function killAllScripts(): void {
for (const server of GetAllServers()) {

@ -541,6 +541,7 @@ export function InteractiveTutorialRoot(): React.ReactElement {
<a
href="https://bitburner-official.readthedocs.io/en/latest/guidesandtips/gettingstartedguideforbeginnerprogrammers.html"
target="_blank"
rel="noreferrer"
>
Getting Started
</a>{" "}
@ -560,7 +561,8 @@ export function InteractiveTutorialRoot(): React.ReactElement {
useEffect(() => {
return ITutorialEvents.subscribe(rerender);
}, []);
}, [rerender]);
const step = ITutorial.currStep;
const content = contents[step];
if (content === undefined) throw new Error("error in the tutorial");

@ -60,12 +60,12 @@ const lineClass = (classes: Record<string, string>, s: string): string => {
return lineClassMap[s] || classes.primary;
};
interface IProps {
type ANSIITypographyProps = {
text: unknown;
color: "primary" | "error" | "success" | "info" | "warn";
}
};
export const ANSIITypography = React.memo((props: IProps): React.ReactElement => {
export const ANSIITypography = React.memo(function ANSIITypography(props: ANSIITypographyProps): React.ReactElement {
const text = String(props.text);
const classes = useStyles();
const parts = [];

@ -40,6 +40,7 @@ import { ActionIdentifier } from "../../Bladeburner/ActionIdentifier";
import { Skills } from "../../PersonObjects/Skills";
import { calculateSkillProgress } from "../../PersonObjects/formulas/skill";
import { EventEmitter } from "../../utils/EventEmitter";
import { useRerender } from "./hooks";
type SkillRowName = "Hack" | "Str" | "Def" | "Dex" | "Agi" | "Cha" | "Int";
type RowName = SkillRowName | "HP" | "Money";
@ -103,8 +104,10 @@ function SkillBar({ name, color }: SkillBarProps): React.ReactElement {
const mult = skillMultUpdaters[name]();
setProgress(calculateSkillProgress(Player.exp[skillNameMap[name]], mult));
});
return clearSubscription;
}, []);
}, [name]);
return (
<TableRow>
<StatsProgressOverviewCell progress={progress} color={color} />
@ -118,11 +121,12 @@ interface ValProps {
}
export function Val({ name, color }: ValProps): React.ReactElement {
//val isn't actually used here, the update of val just forces a refresh of the formattedVal that gets shown
const setVal = useState(valUpdaters[name]())[1];
const [__, setVal] = useState(valUpdaters[name]());
useEffect(() => {
const clearSubscription = OverviewEventEmitter.subscribe(() => setVal(valUpdaters[name]()));
return clearSubscription;
}, []);
}, [name]);
return <Typography color={color}>{formattedVals[name]()}</Typography>;
}
@ -249,11 +253,11 @@ function ActionText(props: { action: ActionIdentifier }): React.ReactElement {
function BladeburnerText(): React.ReactElement {
const classes = useStyles();
const setRerender = useState(false)[1];
const rerender = useRerender();
useEffect(() => {
const clearSubscription = OverviewEventEmitter.subscribe(() => setRerender((old) => !old));
const clearSubscription = OverviewEventEmitter.subscribe(rerender);
return clearSubscription;
}, []);
}, [rerender]);
const action = Player.bladeburner?.action;
return useMemo(
@ -276,7 +280,7 @@ function BladeburnerText(): React.ReactElement {
</TableRow>
</>
),
[action?.type, action?.name, classes.cellNone],
[action, classes.cellNone],
);
}
@ -325,11 +329,11 @@ function WorkInProgressOverview({ tooltip, children, header }: WorkInProgressOve
}
function Work(): React.ReactElement {
const setRerender = useState(false)[1];
const rerender = useRerender();
useEffect(() => {
const clearSubscription = OverviewEventEmitter.subscribe(() => setRerender((old) => !old));
const clearSubscription = OverviewEventEmitter.subscribe(rerender);
return clearSubscription;
}, []);
}, [rerender]);
if (Player.currentWork === null || Player.focus) return <></>;
@ -461,4 +465,4 @@ const useStyles = makeStyles((theme: Theme) =>
}),
);
export { useStyles as characterOverviewStyles };
export { useStyles };

@ -9,57 +9,54 @@ import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
interface IProps {
interface CodingContractProps {
c: CodingContract;
onClose: () => void;
onAttempt: (answer: string) => void;
}
export const CodingContractEvent = new EventEmitter<[IProps]>();
export const CodingContractEvent = new EventEmitter<[CodingContractProps]>();
export function CodingContractModal(): React.ReactElement {
const [props, setProps] = useState<IProps | null>(null);
const [contract, setContract] = useState<CodingContractProps | null>(null);
const [answer, setAnswer] = useState("");
useEffect(() => {
CodingContractEvent.subscribe((props) => setProps(props));
CodingContractEvent.subscribe((props) => setContract(props));
});
if (props === null) return <></>;
if (contract === null) return <></>;
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
setAnswer(event.target.value);
}
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
if (props === null) return;
// React just won't cooperate on this one.
// "React.KeyboardEvent<HTMLInputElement>" seems like the right type but
// whatever ...
const value = (event.target as any).value;
if (contract === null) return;
const value = event.currentTarget.value;
if (event.key === KEY.ENTER && value !== "") {
event.preventDefault();
props.onAttempt(answer);
contract.onAttempt(answer);
setAnswer("");
close();
}
}
function close(): void {
if (props === null) return;
props.onClose();
setProps(null);
if (contract === null) return;
contract.onClose();
setContract(null);
}
const contractType = CodingContractTypes[props.c.type];
const contractType = CodingContractTypes[contract.c.type];
const description = [];
for (const [i, value] of contractType.desc(props.c.data).split("\n").entries())
for (const [i, value] of contractType.desc(contract.c.data).split("\n").entries())
description.push(<span key={i} dangerouslySetInnerHTML={{ __html: value + "<br />" }}></span>);
return (
<Modal open={props !== null} onClose={close}>
<CopyableText variant="h4" value={props.c.type} />
<Modal open={contract !== null} onClose={close}>
<CopyableText variant="h4" value={contract.c.type} />
<Typography>
You are attempting to solve a Coding Contract. You have {props.c.getMaxNumTries() - props.c.tries} tries
You are attempting to solve a Coding Contract. You have {contract.c.getMaxNumTries() - contract.c.tries} tries
remaining, after which the contract will self-destruct.
</Typography>
<br />
@ -75,7 +72,7 @@ export function CodingContractModal(): React.ReactElement {
endAdornment: (
<Button
onClick={() => {
props.onAttempt(answer);
contract.onAttempt(answer);
setAnswer("");
close();
}}

@ -4,7 +4,7 @@ function replace(str: string, i: number, char: string): string {
return str.substring(0, i) + char + str.substring(i + 1);
}
interface IProps {
interface CorruptableTextProps {
content: string;
}
@ -20,7 +20,7 @@ function randomize(char: string): string {
return randFrom(other);
}
export function CorruptableText(props: IProps): JSX.Element {
export function CorruptableText(props: CorruptableTextProps): JSX.Element {
const [content, setContent] = useState(props.content);
useEffect(() => {
@ -30,8 +30,8 @@ export function CorruptableText(props: IProps): JSX.Element {
counter--;
if (counter > 0) return;
counter = Math.random() * 5;
const index = Math.random() * content.length;
const letter = content.charAt(index);
const index = Math.random() * props.content.length;
const letter = props.content.charAt(index);
setContent((content) => replace(content, index, randomize(letter)));
timers.push(
window.setTimeout(() => {
@ -44,7 +44,7 @@ export function CorruptableText(props: IProps): JSX.Element {
clearInterval(intervalId);
timers.forEach((timerId) => clearTimeout(timerId));
};
}, []);
}, [props.content]);
return <span>{content}</span>;
}

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { EventEmitter } from "../../utils/EventEmitter";
import { RunningScript } from "../../Script/RunningScript";
import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
@ -80,6 +80,16 @@ let logs: Log[] = [];
export function LogBoxManager(): React.ReactElement {
const rerender = useRerender();
//Close tail windows by their pid.
const closePid = useCallback(
(pid: number) => {
logs = logs.filter((log) => log.script.pid !== pid);
rerender();
},
[rerender],
);
useEffect(
() =>
LogBoxEvents.subscribe((script: RunningScript) => {
@ -90,7 +100,7 @@ export function LogBoxManager(): React.ReactElement {
});
rerender();
}),
[],
[rerender],
);
//Event used by ns.closeTail to close tail windows
@ -99,14 +109,16 @@ export function LogBoxManager(): React.ReactElement {
LogBoxCloserEvents.subscribe((pid: number) => {
closePid(pid);
}),
[],
[closePid],
);
useEffect(() =>
LogBoxClearEvents.subscribe(() => {
logs = [];
rerender();
}),
useEffect(
() =>
LogBoxClearEvents.subscribe(() => {
logs = [];
rerender();
}),
[rerender],
);
//Close tail windows by their id
@ -115,12 +127,6 @@ export function LogBoxManager(): React.ReactElement {
rerender();
}
//Close tail windows by their pid.
function closePid(pid: number): void {
logs = logs.filter((log) => log.script.pid !== pid);
rerender();
}
return (
<>
{logs.map((log) => (
@ -130,7 +136,7 @@ export function LogBoxManager(): React.ReactElement {
);
}
interface IProps {
interface LogWindowProps {
script: RunningScript;
onClose: () => void;
}
@ -158,7 +164,7 @@ const useStyles = makeStyles(() =>
export const logBoxBaseZIndex = 1500;
function LogWindow(props: IProps): React.ReactElement {
function LogWindow(props: LogWindowProps): React.ReactElement {
const draggableRef = useRef<HTMLDivElement>(null);
const rootRef = useRef<Draggable>(null);
const script = props.script;
@ -187,10 +193,18 @@ function LogWindow(props: IProps): React.ReactElement {
propsRef.current.setSize(size.width, size.height);
};
const updateLayer = useCallback(() => {
const c = container.current;
if (c === null) return;
c.style.zIndex = logBoxBaseZIndex + layerCounter + "";
layerCounter++;
rerender();
}, [rerender]);
useEffect(() => {
propsRef.current.updateDOM();
updateLayer();
}, []);
}, [updateLayer]);
function kill(): void {
killWorkerScriptByPid(script.pid);
@ -226,14 +240,6 @@ function LogWindow(props: IProps): React.ReactElement {
}
}
function updateLayer(): void {
const c = container.current;
if (c === null) return;
c.style.zIndex = logBoxBaseZIndex + layerCounter + "";
layerCounter++;
rerender();
}
function title(): React.ReactElement {
const title_str = script.title === "string" ? script.title : `${script.filename} ${script.args.join(" ")}`;
return (
@ -267,22 +273,26 @@ function LogWindow(props: IProps): React.ReactElement {
return "primary";
}
const onWindowResize = useMemo(
() =>
debounce((): void => {
const node = draggableRef.current;
if (!node) return;
if (!isOnScreen(node)) {
propsRef.current.setPosition(0, 0);
}
}, 100),
[],
);
// And trigger fakeDrag when the window is resized
useEffect(() => {
window.addEventListener("resize", onWindowResize);
return () => {
window.removeEventListener("resize", onWindowResize);
};
}, []);
const onWindowResize = debounce((): void => {
const node = draggableRef.current;
if (!node) return;
if (!isOnScreen(node)) {
propsRef.current.setPosition(0, 0);
}
}, 100);
}, [onWindowResize]);
const isOnScreen = (node: HTMLDivElement): boolean => {
const bounds = node.getBoundingClientRect();

@ -1,22 +1,22 @@
import { FormControlLabel, Switch, Tooltip, Typography } from "@mui/material";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
interface IProps {
type OptionSwitchProps = {
checked: boolean;
onChange: (newValue: boolean, error?: string) => void;
text: React.ReactNode;
tooltip: React.ReactNode;
}
};
export function OptionSwitch({ checked, onChange, text, tooltip }: IProps): React.ReactElement {
export function OptionSwitch({ checked, onChange, text, tooltip }: OptionSwitchProps): React.ReactElement {
const [value, setValue] = useState(checked);
function handleSwitchChange(event: React.ChangeEvent<HTMLInputElement>): void {
setValue(event.target.checked);
const newValue = event.target.checked;
setValue(newValue);
onChange(newValue);
}
useEffect(() => onChange(value), [value]);
return (
<>
<FormControlLabel

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useMemo } from "react";
import Draggable, { DraggableEventHandler } from "react-draggable";
import makeStyles from "@mui/styles/makeStyles";
import Collapse from "@mui/material/Collapse";
@ -82,8 +82,25 @@ export function Overview({ children, mode }: IProps): React.ReactElement {
Settings.overview = { x, y, opened: open };
}, [open, x, y]);
const fakeDrag = useMemo(
() =>
debounce((): void => {
const node = draggableRef.current;
if (!node) return;
// No official way to trigger an onChange to recompute the bounds
// See: https://github.com/react-grid-layout/react-draggable/issues/363#issuecomment-947751127
triggerMouseEvent(node, "mouseover");
triggerMouseEvent(node, "mousedown");
triggerMouseEvent(document, "mousemove");
triggerMouseEvent(node, "mouseup");
triggerMouseEvent(node, "click");
}, 100),
[],
);
// Trigger fakeDrag once to make sure loaded data is not outside bounds
useEffect(() => fakeDrag(), []);
useEffect(() => fakeDrag(), [fakeDrag]);
// And trigger fakeDrag when the window is resized
useEffect(() => {
@ -91,20 +108,7 @@ export function Overview({ children, mode }: IProps): React.ReactElement {
return () => {
window.removeEventListener("resize", fakeDrag);
};
}, []);
const fakeDrag = debounce((): void => {
const node = draggableRef.current;
if (!node) return;
// No official way to trigger an onChange to recompute the bounds
// See: https://github.com/react-grid-layout/react-draggable/issues/363#issuecomment-947751127
triggerMouseEvent(node, "mouseover");
triggerMouseEvent(node, "mousedown");
triggerMouseEvent(document, "mousemove");
triggerMouseEvent(node, "mouseup");
triggerMouseEvent(node, "click");
}, 100);
}, [fakeDrag]);
const triggerMouseEvent = (node: HTMLDivElement | Document, eventType: string): void => {
const clickEvent = document.createEvent("MouseEvents");

@ -1,7 +1,7 @@
import * as React from "react";
import LinearProgress from "@mui/material/LinearProgress";
import { TableCell, Tooltip, Typography } from "@mui/material";
import { characterOverviewStyles } from "./CharacterOverview";
import { useStyles } from "./CharacterOverview";
import { ISkillProgress } from "../../PersonObjects/formulas/skill";
import { formatExp } from "../formatNumber";
@ -54,7 +54,7 @@ export function StatsProgressBar({
}
export function StatsProgressOverviewCell({ progress: skill, color }: IStatsOverviewCellProps): React.ReactElement {
const classes = characterOverviewStyles();
const classes = useStyles();
return (
<TableCell
component="th"

@ -3,8 +3,7 @@ import React from "react";
import { Typography, TableCell, TableRow } from "@mui/material";
import { formatExp, formatNumberNoSuffix } from "../formatNumber";
import { characterOverviewStyles as useStyles } from "./CharacterOverview";
import { ClassNameMap } from "@material-ui/core/styles/withStyles";
import { useStyles } from "./CharacterOverview";
interface ITableRowData {
content?: string;
@ -15,14 +14,14 @@ interface ITableRowData {
interface IProps {
name: string;
color: string;
classes?: ClassNameMap;
data?: ITableRowData;
children?: React.ReactElement;
}
export const StatsRow = ({ name, color, classes = useStyles(), children, data }: IProps): React.ReactElement => {
let content = "";
export const StatsRow = ({ name, color, children, data }: IProps): React.ReactElement => {
const classes = useStyles();
let content = "";
if (data) {
if (data.content !== undefined) {
content = data.content;
@ -39,7 +38,7 @@ export const StatsRow = ({ name, color, classes = useStyles(), children, data }:
<Typography style={{ color: color }}>{name}</Typography>
</TableCell>
<TableCell align="right" classes={{ root: classes.cellNone }}>
{content ? <Typography style={{ color: color }}>{content}</Typography> : <></>}
{content && <Typography style={{ color: color }}>{content}</Typography>}
{children}
</TableCell>
</TableRow>

@ -4,13 +4,16 @@ import { useCallback, useEffect, useState } from "react";
* @param autoRerenderTime: Optional. If provided and nonzero, used as the ms interval to automatically call the rerender function.
*/
export function useRerender(autoRerenderTime?: number) {
const setRerender = useState(false)[1];
const rerender = () => setRerender((old) => !old);
const [__, setRerender] = useState(false);
const rerender = useCallback(() => setRerender((old) => !old), []);
useEffect(() => {
if (!autoRerenderTime) return;
const intervalID = setInterval(rerender, autoRerenderTime);
return () => clearInterval(intervalID);
}, []);
}, [rerender, autoRerenderTime]);
return rerender;
}

@ -17,5 +17,5 @@
"strict": true,
"target": "es2022"
},
"include": ["src/**/*", "electron/**/*", ".eslintrc.js", "node_modules/monaco-editor/monaco.d.ts"]
"include": ["src/**/*", "electron/**/*", "node_modules/monaco-editor/monaco.d.ts"]
}