mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2025-03-07 11:04:36 +01:00
UI: Sync UI updates to game updates. (#1512)
There are a bunch of React components that update at the same rate that the game engine processes cycles. Rather than have each place that does so start its own timer to update that often, add a new react hook that triggers an update shortly after the engine completes a cycle.
This commit is contained in:
@ -26,7 +26,7 @@ import { formatNumberNoSuffix } from "../../ui/formatNumber";
|
||||
import { Info } from "@mui/icons-material";
|
||||
import { Link } from "@mui/material";
|
||||
import { AlertEvents } from "../../ui/React/AlertManager";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
const NeuroFluxDisplay = (): React.ReactElement => {
|
||||
const level = Player.augmentations.find((e) => e.name === AugmentationName.NeuroFluxGovernor)?.level ?? 0;
|
||||
@ -85,7 +85,7 @@ interface IProps {
|
||||
|
||||
export function AugmentationsRoot(props: IProps): React.ReactElement {
|
||||
const [installOpen, setInstallOpen] = useState(false);
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
|
||||
function doExport(): void {
|
||||
props.exportGameFn();
|
||||
|
@ -5,10 +5,10 @@ import { AllPages } from "./AllPages";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { Box } from "@mui/material";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
export function BladeburnerRoot(): React.ReactElement {
|
||||
useRerender(200);
|
||||
useCycleRerender();
|
||||
const bladeburner = Player.bladeburner;
|
||||
if (!bladeburner) return <></>;
|
||||
return (
|
||||
|
@ -10,10 +10,10 @@ import { Overview } from "./Overview";
|
||||
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
export function CorporationRoot(): React.ReactElement {
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
const [divisionName, setDivisionName] = useState<string | number>("Overview");
|
||||
|
||||
const corporation = Player.corporation;
|
||||
|
@ -20,7 +20,7 @@ import { CovenantPurchasesRoot } from "../../PersonObjects/Sleeve/ui/CovenantPur
|
||||
import { FactionName, FactionWorkType } from "@enums";
|
||||
import { GangButton } from "./GangButton";
|
||||
import { FactionWork } from "../../Work/FactionWork";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
import { repNeededToDonate } from "../formulas/donation";
|
||||
|
||||
type FactionRootProps = {
|
||||
@ -147,7 +147,7 @@ function MainPage({ faction, rerender, onAugmentations }: IMainProps): React.Rea
|
||||
}
|
||||
|
||||
export function FactionRoot({ faction }: FactionRootProps): React.ReactElement {
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
|
||||
if (!Player.factions.includes(faction.name)) {
|
||||
return (
|
||||
|
@ -9,7 +9,7 @@ import { Settings } from "../../Settings/Settings";
|
||||
import { formatFavor, formatReputation } from "../../ui/formatNumber";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { Requirement } from "../../ui/Components/Requirement";
|
||||
|
||||
@ -205,7 +205,7 @@ const FactionElement = (props: FactionElementProps): React.ReactElement => {
|
||||
|
||||
export function FactionsRoot(): React.ReactElement {
|
||||
const theme = useTheme();
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
useEffect(() => {
|
||||
Player.factionInvitations.forEach((factionName) => {
|
||||
InvitationsSeen.add(factionName);
|
||||
|
@ -15,7 +15,7 @@ import { makeStyles } from "tss-react/mui";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
import { calculateFavorAfterResetting } from "../formulas/favor";
|
||||
|
||||
interface IProps {
|
||||
@ -42,7 +42,7 @@ function DefaultAssignment(): React.ReactElement {
|
||||
}
|
||||
|
||||
export function Info(props: IProps): React.ReactElement {
|
||||
useRerender(200);
|
||||
useCycleRerender();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const Assignment = props.factionInfo.assignment ?? DefaultAssignment;
|
||||
|
@ -8,7 +8,7 @@ import { Context } from "./Context";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
/** React Component for all the gang stuff. */
|
||||
export function GangRoot(): React.ReactElement {
|
||||
@ -22,7 +22,7 @@ export function GangRoot(): React.ReactElement {
|
||||
setValue(tab);
|
||||
}
|
||||
|
||||
useRerender(200);
|
||||
useCycleRerender();
|
||||
|
||||
return (
|
||||
<Context.Gang.Provider value={gang}>
|
||||
|
@ -25,12 +25,12 @@ import Typography from "@mui/material/Typography";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Box } from "@mui/material";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
/** Root React Component for the Hacknet Node UI */
|
||||
export function HacknetRoot(): React.ReactElement {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
const [purchaseMultiplier, setPurchaseMultiplier] = useState<number | "MAX">(PurchaseMultipliers.x1);
|
||||
|
||||
let totalProduction = 0;
|
||||
|
@ -7,7 +7,7 @@ import { HacknetUpgradeElem } from "./HacknetUpgradeElem";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { Player } from "@player";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
@ -16,7 +16,7 @@ interface IProps {
|
||||
|
||||
/** Create the pop-up for purchasing upgrades with hashes */
|
||||
export function HashUpgradeModal(props: IProps): React.ReactElement {
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
|
||||
const hashManager = Player.hashManager;
|
||||
if (!hashManager) {
|
||||
|
@ -19,7 +19,7 @@ import { Page } from "../../ui/Router";
|
||||
import { Player } from "@player";
|
||||
import { QuitJobModal } from "../../Company/ui/QuitJobModal";
|
||||
import { CompanyWork } from "../../Work/CompanyWork";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
import { companyNameAsLocationName } from "../../Company/utils";
|
||||
import { JobSummary } from "../../Company/ui/JobSummary";
|
||||
import { StatsTable } from "../../ui/React/StatsTable";
|
||||
@ -32,7 +32,7 @@ interface IProps {
|
||||
|
||||
export function CompanyLocation(props: IProps): React.ReactElement {
|
||||
const [quitOpen, setQuitOpen] = useState(false);
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
|
||||
/**
|
||||
* We'll keep a reference to the Company that this component is being rendered for,
|
||||
|
@ -12,12 +12,12 @@ import { getHospitalizationCost } from "../../Hospital/Hospital";
|
||||
import { Money } from "../../ui/React/Money";
|
||||
|
||||
import { dialogBoxCreate } from "../../ui/React/DialogBox";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
export function HospitalLocation(): React.ReactElement {
|
||||
/** Stores button styling that sets them all to block display */
|
||||
const btnStyle = { display: "block" };
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
|
||||
function getHealed(e: React.MouseEvent<HTMLElement>): void {
|
||||
if (!e.isTrusted) {
|
||||
|
@ -21,7 +21,7 @@ import { formatNumberNoSuffix } from "../../../ui/formatNumber";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
|
||||
import { GraftableAugmentation } from "../GraftableAugmentation";
|
||||
import { calculateGraftingTimeWithBonus, getGraftingAvailableAugs } from "../GraftingHelpers";
|
||||
import { useRerender } from "../../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../../ui/React/hooks";
|
||||
|
||||
export const GraftableAugmentations = (): Record<string, GraftableAugmentation> => {
|
||||
const gAugs: Record<string, GraftableAugmentation> = {};
|
||||
@ -67,7 +67,7 @@ export const GraftingRoot = (): React.ReactElement => {
|
||||
const [selectedAug, setSelectedAug] = useState(getGraftingAvailableAugs()[0]);
|
||||
const [graftOpen, setGraftOpen] = useState(false);
|
||||
const selectedAugmentation = Augmentations[selectedAug];
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
|
||||
const getAugsSorted = (): AugmentationName[] => {
|
||||
const augs = getGraftingAvailableAugs();
|
||||
|
@ -6,11 +6,11 @@ import { Player } from "@player";
|
||||
|
||||
import { SleeveElem } from "./SleeveElem";
|
||||
import { FAQModal } from "./FAQModal";
|
||||
import { useRerender } from "../../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../../ui/React/hooks";
|
||||
|
||||
export function SleeveRoot(): React.ReactElement {
|
||||
const [FAQOpen, setFAQOpen] = useState(false);
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -11,12 +11,12 @@ import { Settings } from "../../Settings/Settings";
|
||||
|
||||
import { Programs } from "../Programs";
|
||||
import { CreateProgramWork, isCreateProgramWork } from "../../Work/CreateProgramWork";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
export const ProgramsSeen = new Set<string>();
|
||||
|
||||
export function ProgramsRoot(): React.ReactElement {
|
||||
useRerender(200);
|
||||
useCycleRerender();
|
||||
|
||||
const programs = [...Object.values(Programs)]
|
||||
.filter((prog) => {
|
||||
|
@ -54,7 +54,7 @@ import { ProgramsSeen } from "../../Programs/ui/ProgramsRoot";
|
||||
import { InvitationsSeen } from "../../Faction/ui/FactionsRoot";
|
||||
import { hash } from "../../hash/hash";
|
||||
import { Locations } from "../../Locations/Locations";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
import { playerHasDiscoveredGo } from "../../Go/effects/effect";
|
||||
import { knowAboutBitverse } from "../../BitNode/BitNodeUtils";
|
||||
|
||||
@ -108,7 +108,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
|
||||
}));
|
||||
|
||||
export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
useRerender(200);
|
||||
useCycleRerender();
|
||||
|
||||
let flash: Page | null = null;
|
||||
switch (ITutorial.currStep) {
|
||||
|
@ -6,7 +6,7 @@ import { StockTickers } from "./StockTickers";
|
||||
import { IStockMarket } from "../IStockMarket";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
|
||||
interface IProps {
|
||||
stockMarket: IStockMarket;
|
||||
@ -14,7 +14,7 @@ interface IProps {
|
||||
|
||||
/** Root React component for the Stock Market UI */
|
||||
export function StockMarketRoot(props: IProps): React.ReactElement {
|
||||
const rerender = useRerender(200);
|
||||
const rerender = useCycleRerender();
|
||||
return (
|
||||
<>
|
||||
<InfoAndPurchases rerender={rerender} />
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { useCycleRerender } from "../../ui/React/hooks";
|
||||
import { Terminal } from "../../Terminal";
|
||||
|
||||
export function TerminalActionTimer(): React.ReactElement {
|
||||
useRerender(200);
|
||||
useCycleRerender();
|
||||
|
||||
return <Typography color="primary">{Terminal.action && Terminal.getProgressText()}</Typography>;
|
||||
}
|
||||
|
@ -39,11 +39,13 @@ import { startExploits } from "./Exploits/loops";
|
||||
import { calculateAchievements } from "./Achievements/Achievements";
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { SnackbarEvents } from "./ui/React/Snackbar";
|
||||
import { SaveData } from "./types";
|
||||
import { Go } from "./Go/Go";
|
||||
import { EventEmitter } from "./utils/EventEmitter";
|
||||
|
||||
// Only show warning if the time diff is greater than this value.
|
||||
const thresholdOfTimeDiffForShowingWarningAboutSystemClock = CONSTANTS.MillisecondsPerFiveMinutes;
|
||||
@ -54,6 +56,8 @@ function showWarningAboutSystemClock(timeDiff: number) {
|
||||
);
|
||||
}
|
||||
|
||||
export const GameCycleEvents = new EventEmitter<[]>();
|
||||
|
||||
/** Game engine. Handles the main game loop. */
|
||||
const Engine: {
|
||||
_lastUpdate: number;
|
||||
@ -428,6 +432,11 @@ const Engine: {
|
||||
Engine._lastUpdate = _thisUpdate - offset;
|
||||
Player.lastUpdate = _thisUpdate - offset;
|
||||
Engine.updateGame(diff);
|
||||
if (GameCycleEvents.hasSubscibers()) {
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
GameCycleEvents.emit();
|
||||
});
|
||||
}
|
||||
}
|
||||
window.setTimeout(Engine.start, CONSTANTS.MilliPerCycle - offset);
|
||||
},
|
||||
|
@ -15,7 +15,7 @@ import { Modal } from "./React/Modal";
|
||||
import { Money } from "./React/Money";
|
||||
import { StatsRow } from "./React/StatsRow";
|
||||
import { StatsTable } from "./React/StatsTable";
|
||||
import { useRerender } from "./React/hooks";
|
||||
import { useCycleRerender } from "./React/hooks";
|
||||
import { getMaxFavor } from "../Go/effects/effect";
|
||||
import { canAccessBitNodeFeature, knowAboutBitverse } from "../BitNode/BitNodeUtils";
|
||||
|
||||
@ -220,7 +220,7 @@ function MoneyModal({ open, onClose }: IMoneyModalProps): React.ReactElement {
|
||||
export function CharacterStats(): React.ReactElement {
|
||||
const [moneyOpen, setMoneyOpen] = useState(false);
|
||||
const [employersOpen, setEmployersOpen] = useState(false);
|
||||
useRerender(200);
|
||||
useCycleRerender();
|
||||
|
||||
const timeRows = [
|
||||
["Since last Augmentation installation", convertTimeMsToTimeElapsedString(Player.playtimeSinceLastAug)],
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { GameCycleEvents } from "../../engine";
|
||||
|
||||
/** Hook that returns a function for the component. Optionally set an interval to rerender the component.
|
||||
* @param autoRerenderTime: Optional. If provided and nonzero, used as the ms interval to automatically call the rerender function.
|
||||
@ -17,6 +18,19 @@ export function useRerender(autoRerenderTime?: number) {
|
||||
return rerender;
|
||||
}
|
||||
|
||||
/** Hook that rerenders the component shortly after the game engine processes a cycle.
|
||||
* @returns a function that will trigger a rerender.
|
||||
*/
|
||||
export function useCycleRerender(): () => void {
|
||||
const rerender = useRerender();
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = GameCycleEvents.subscribe(rerender);
|
||||
return unsubscribe;
|
||||
}, [rerender]);
|
||||
return rerender;
|
||||
}
|
||||
|
||||
export function useBoolean(initialValue = false) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
|
@ -10,7 +10,7 @@ import { ProgressBar } from "./React/Progress";
|
||||
import { Reputation } from "./React/Reputation";
|
||||
import { ReputationRate } from "./React/ReputationRate";
|
||||
import { StatsRow } from "./React/StatsRow";
|
||||
import { useRerender } from "./React/hooks";
|
||||
import { useCycleRerender } from "./React/hooks";
|
||||
|
||||
import { Companies } from "../Company/Companies";
|
||||
import { CONSTANTS } from "../Constants";
|
||||
@ -183,7 +183,7 @@ function CrimeExpRows(rate: WorkStats): React.ReactElement[] {
|
||||
}
|
||||
|
||||
export function WorkInProgressRoot(): React.ReactElement {
|
||||
useRerender(CONSTANTS.MilliPerCycle);
|
||||
useCycleRerender();
|
||||
|
||||
let workInfo: IWorkInfo = {
|
||||
buttons: {
|
||||
|
@ -1,31 +1,24 @@
|
||||
function uuidv4(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0,
|
||||
v = c == "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/** Generic Event Emitter class following a subscribe/publish paradigm. */
|
||||
export class EventEmitter<T extends any[]> {
|
||||
subscribers: Record<string, (...args: [...T]) => void | undefined> = {};
|
||||
private subscribers: Set<(...args: [...T]) => void | undefined> = new Set();
|
||||
|
||||
constructor() {}
|
||||
|
||||
subscribe(s: (...args: [...T]) => void): () => void {
|
||||
let uuid = uuidv4();
|
||||
while (this.subscribers[uuid] !== undefined) uuid = uuidv4();
|
||||
this.subscribers[uuid] = s;
|
||||
this.subscribers.add(s);
|
||||
|
||||
return () => {
|
||||
delete this.subscribers[uuid];
|
||||
this.subscribers.delete(s);
|
||||
};
|
||||
}
|
||||
|
||||
emit(...args: [...T]): void {
|
||||
for (const s in this.subscribers) {
|
||||
const sub = this.subscribers[s];
|
||||
if (sub === undefined) continue;
|
||||
|
||||
for (const sub of this.subscribers) {
|
||||
sub(...args);
|
||||
}
|
||||
}
|
||||
|
||||
hasSubscibers(): boolean {
|
||||
return this.subscribers.size > 0;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user