diff --git a/src/Bladeburner/ui/BladeburnerCinematic.tsx b/src/Bladeburner/ui/BladeburnerCinematic.tsx index 49faf0927..42e8c843c 100644 --- a/src/Bladeburner/ui/BladeburnerCinematic.tsx +++ b/src/Bladeburner/ui/BladeburnerCinematic.tsx @@ -3,7 +3,6 @@ import { FactionName } from "@enums"; import { Router } from "../../ui/GameRoot"; import { Page } from "../../ui/Router"; import { CinematicText } from "../../ui/React/CinematicText"; -import { dialogBoxCreate } from "../../ui/React/DialogBox"; export function BladeburnerCinematic(): React.ReactElement { return ( @@ -33,10 +32,6 @@ export function BladeburnerCinematic(): React.ReactElement { ]} onDone={() => { Router.toPage(Page.Terminal); - dialogBoxCreate( - `Visit the National Security Agency (NSA) to apply for their ${FactionName.Bladeburners} ` + - "division! You will need 100 of each combat stat before doing this.", - ); }} /> ); diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index f597cd66b..47b244288 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -1648,7 +1648,7 @@ export const ns: InternalAPI = { }, alert: (ctx) => (_message) => { const message = helpers.string(ctx, "message", _message); - dialogBoxCreate(message, true); + dialogBoxCreate(message, { html: true, canBeDismissedEasily: true }); }, toast: (ctx) => diff --git a/src/Prestige.ts b/src/Prestige.ts index 3cde0a85d..9cf707bc9 100644 --- a/src/Prestige.ts +++ b/src/Prestige.ts @@ -32,8 +32,8 @@ import { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils"; import { pendingUIShareJobIds } from "./NetworkShare/Share"; const BitNode8StartingMoney = 250e6; -function delayedDialog(message: string) { - setTimeout(() => dialogBoxCreate(message), 200); +function delayedDialog(message: string, canBeDismissedEasily = true) { + setTimeout(() => dialogBoxCreate(message, { html: false, canBeDismissedEasily }), 200); } function setInitialExpForPlayer() { @@ -268,12 +268,16 @@ export function prestigeSourceFile(isFlume: boolean): void { "You received a copy of the Corporation Management Handbook on your home computer. It's a short introduction for " + "managing Corporation.\n\nYou should check the in-game Corporation documentation in the Documentation tab " + "(Documentation -> Advanced Mechanics -> Corporation). It's the most useful and up-to-date resource for managing Corporation.", + false, ); } // BitNode 6: Bladeburners and BitNode 7: Bladeburners 2079 if (Player.bitNodeN === 6 || Player.bitNodeN === 7) { - delayedDialog(`The ${CompanyName.NSA} would like to have a word with you once you're ready.`); + delayedDialog( + `The ${CompanyName.NSA} would like to have a word with you once you're ready. You should train your combat stats to level 100 before going there.`, + false, + ); } // BitNode 8: Ghost of Wall Street @@ -289,6 +293,7 @@ export function prestigeSourceFile(isFlume: boolean): void { if (Player.bitNodeN === 10) { delayedDialog( `Seek out ${FactionName.TheCovenant} if you'd like to purchase a new sleeve or two! And see what ${CompanyName.VitaLife} in ${CityName.NewTokyo} has to offer for you`, + false, ); } @@ -298,7 +303,7 @@ export function prestigeSourceFile(isFlume: boolean): void { } if (Player.bitNodeN === 13) { - delayedDialog(`Trouble is brewing in ${CityName.Chongqing}`); + delayedDialog(`Trouble is brewing in ${CityName.Chongqing}`, false); } // Reset Stock market, gang, and corporation @@ -343,6 +348,7 @@ export function prestigeSourceFile(isFlume: boolean): void { if (!isFlume && Player.sourceFiles.size === 1 && Player.sourceFileLvl(1) === 1) { delayedDialog( "Congratulations on destroying your first BitNode! Make sure to check the Documentation tab. Many pages are unlocked now.", + false, ); } } diff --git a/src/ui/React/AlertManager.tsx b/src/ui/React/AlertManager.tsx index b0603859b..8f153318e 100644 --- a/src/ui/React/AlertManager.tsx +++ b/src/ui/React/AlertManager.tsx @@ -4,31 +4,34 @@ import { Modal } from "./Modal"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import { cyrb53 } from "../../utils/HashUtils"; +import Button from "@mui/material/Button"; -export const AlertEvents = new EventEmitter<[string | JSX.Element]>(); +export const AlertEvents = new EventEmitter<[string | JSX.Element, boolean?]>(); interface Alert { text: string | JSX.Element; hash: string; + /** + * If it's true, the player can dismiss the modal by pressing the Esc button or clicking on the backdrop. + * + * Note that there are 2 different behaviors when pressing the Esc button, depending on whether the player focused on + * the modal. If they focused on the modal and canBeDismissedEasily is false, the modal would not be dismissed. If + * they did not, pressing the Esc button would always dismiss **all** popups in the queue maintained by this manager. + */ + canBeDismissedEasily: boolean; } export function AlertManager({ hidden }: { hidden: boolean }): React.ReactElement { const [alerts, setAlerts] = useState([]); useEffect( () => - AlertEvents.subscribe((text: string | JSX.Element) => { + AlertEvents.subscribe((text: string | JSX.Element, canBeDismissedEasily = true) => { const hash = getMessageHash(text); setAlerts((old) => { if (old.some((a) => a.hash === hash)) { return old; } - return [ - ...old, - { - text: text, - hash: hash, - }, - ]; + return [...old, { text, hash, canBeDismissedEasily }]; }); }), [], @@ -36,15 +39,17 @@ export function AlertManager({ hidden }: { hidden: boolean }): React.ReactElemen useEffect(() => { function handle(this: Document, event: KeyboardEvent): void { - if (event.code === "Escape") { - setAlerts([]); + if (event.code !== "Escape") { + return; } + setAlerts([]); } document.addEventListener("keydown", handle); return () => document.removeEventListener("keydown", handle); - }, []); + }, [alerts]); const alertMessage = alerts[0]?.text || "No alert to show"; + const canBeDismissedEasily = alerts[0]?.canBeDismissedEasily; function getMessageHash(text: string | JSX.Element): string { if (typeof text === "string") { @@ -75,10 +80,15 @@ export function AlertManager({ hidden }: { hidden: boolean }): React.ReactElemen } return ( - 0} onClose={close}> + 0} onClose={close} canBeDismissedEasily={canBeDismissedEasily}> {alertMessage} + {!canBeDismissedEasily && ( + + )} ); } diff --git a/src/ui/React/DialogBox.tsx b/src/ui/React/DialogBox.tsx index d2c70db8f..cca786a0c 100644 --- a/src/ui/React/DialogBox.tsx +++ b/src/ui/React/DialogBox.tsx @@ -3,7 +3,10 @@ import { AlertEvents } from "./AlertManager"; import React from "react"; import { Typography } from "@mui/material"; -export function dialogBoxCreate(txt: string | JSX.Element, html = false): void { +export function dialogBoxCreate( + txt: string | JSX.Element, + { html, canBeDismissedEasily } = { html: false, canBeDismissedEasily: true }, +): void { AlertEvents.emit( typeof txt !== "string" ? ( txt @@ -14,5 +17,6 @@ export function dialogBoxCreate(txt: string | JSX.Element, html = false): void { {txt} ), + canBeDismissedEasily, ); } diff --git a/src/ui/React/Modal.tsx b/src/ui/React/Modal.tsx index 2d0995b45..5decc0261 100644 --- a/src/ui/React/Modal.tsx +++ b/src/ui/React/Modal.tsx @@ -44,9 +44,18 @@ interface ModalProps { children: React.ReactNode; sx?: SxProps; removeFocus?: boolean; + // If it's true, the player can dismiss the modal by pressing the Esc button or clicking on the backdrop. + canBeDismissedEasily?: boolean; } -export const Modal = ({ open, onClose, children, sx, removeFocus = true }: ModalProps): React.ReactElement => { +export const Modal = ({ + open, + onClose, + children, + sx, + removeFocus = true, + canBeDismissedEasily = true, +}: ModalProps): React.ReactElement => { const { classes } = useStyles(); const [content, setContent] = useState(children); useEffect(() => { @@ -61,7 +70,12 @@ export const Modal = ({ open, onClose, children, sx, removeFocus = true }: Modal disableEnforceFocus disableAutoFocus={removeFocus} open={open} - onClose={onClose} + onClose={() => { + if (!canBeDismissedEasily) { + return; + } + onClose(); + }} closeAfterTransition className={classes.modal} sx={sx}