diff --git a/src/GameOptions/ui/CreditsModal.tsx b/src/GameOptions/ui/CreditsModal.tsx
new file mode 100644
index 000000000..b894865c9
--- /dev/null
+++ b/src/GameOptions/ui/CreditsModal.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { Modal } from "../../ui/React/Modal";
+import { Typography, Link, Button } from "@mui/material";
+
+import { CONSTANTS } from "../../Constants";
+
+interface CreditsModalProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+const enclosed = /(\([^)]+\))/gm; //grab all filled () pairs
+const recentPatchData = Array.from(new Set(CONSTANTS.LatestUpdate.match(enclosed)));
+
+const isDate = (data: string) => {
+ const regex = /^\(last update/gm; //(this) isn't @name, but may be useful
+ return regex.test(data);
+};
+const updateMessage = []; //store last update message, eg (last updated 9/12/23)
+if (isDate(recentPatchData[0])) updateMessage.push(recentPatchData[0]);
+
+const handle: string[] = [];
+for (let i = 0; i < recentPatchData.length; i++) {
+ const atName = /(?:^[(]?(@[^\s),]+)[),]?)/gm; //make an array of only unique @handles
+ const whatWeWant = recentPatchData[i].replace(atName, "$1");
+ if (isDate(recentPatchData[i]) || !recentPatchData[i].includes("@")) continue;
+ if (recentPatchData[i].includes(", ")) {
+ //if (@1, @2, ...@n)
+ recentPatchData.push(...recentPatchData[i].split(", "));
+ continue;
+ }
+ if (!handle.includes(whatWeWant)) handle.push(whatWeWant);
+}
+
+export function CreditsModal(props: CreditsModalProps): React.ReactElement {
+ const leadDevs = `danielyxie
+Olivier Gagnon
+@Snarling
+`;
+
+ const currentMaintainer = `@Snarling`;
+
+ const handles = handle.sort((a, b) => a.localeCompare(b)).join(", ");
+ const contributorsURL = `https://github.com/bitburner-official/bitburner-src/graphs/contributors`;
+ const contributorsMessage = `Visit GitHub to see all contributors
+or to participate yourself`;
+ const maxEM = Math.floor(contributorsMessage.length / 2);
+
+ return (
+
+
+ Bitburner
+
+ Original Code and Concept
+ danielyxie
+
+ Lead Developers:
+ {leadDevs}
+
+ Current Maintainer
+ {currentMaintainer}
+
+ Recent patch contributors:
+
+ {/*rem unit = character px based, dynamic with font. balance contributor overflow vs longest other message*/}
+ {/*textoverflow "clip" forces very long @names to stretch a single line, it's silly*/}
+ {handles}
+
+
+
+
+ {contributorsMessage}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/GameOptions/ui/GameOptionsSidebar.tsx b/src/GameOptions/ui/GameOptionsSidebar.tsx
index 4f2a7b4e6..2a5301d3b 100644
--- a/src/GameOptions/ui/GameOptionsSidebar.tsx
+++ b/src/GameOptions/ui/GameOptionsSidebar.tsx
@@ -1,4 +1,14 @@
-import { BugReport, Chat, Download, LibraryBooks, Palette, Reddit, Save, Upload } from "@mui/icons-material";
+import {
+ BugReport,
+ Chat,
+ Download,
+ LibraryBooks,
+ Palette,
+ Fingerprint,
+ Reddit,
+ Save,
+ Upload,
+} from "@mui/icons-material";
import { Box, Button, List, ListItemButton, Paper, Tooltip, Typography } from "@mui/material";
import { default as React, useRef, useState } from "react";
import { FileDiagnosticModal } from "../../Diagnostic/FileDiagnosticModal";
@@ -6,6 +16,7 @@ import { ImportData, saveObject } from "../../SaveObject";
import { StyleEditorButton } from "../../Themes/ui/StyleEditorButton";
import { ThemeEditorButton } from "../../Themes/ui/ThemeEditorButton";
import { ConfirmationModal } from "../../ui/React/ConfirmationModal";
+import { CreditsModal } from "./CreditsModal";
import { DeleteGameButton } from "../../ui/React/DeleteGameButton";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { ToastVariant } from "@enums";
@@ -49,6 +60,7 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
const [importData, setImportData] = useState(null);
const [confirmResetOpen, setConfirmResetOpen] = useState(false);
+ const [creditsOpen, setCreditsOpen] = useState(false);
function startImport(): void {
if (!window.File || !window.FileReader || !window.FileList || !window.Blob) return;
@@ -225,7 +237,8 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
sx={{
gridArea: "links",
display: "grid",
- gridTemplateAreas: `"bug bug"
+ gridTemplateAreas: `"credits credits"
+ "bug bug"
"discord reddit"
"tut tut"
"plaza plaza"`,
@@ -233,14 +246,20 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => {
my: 1,
}}
>
- }
- href="https://github.com/bitburner-official/bitburner-src/issues/new"
- target="_blank"
- sx={{ gridArea: "bug" }}
- >
- Report Bug
+ Start a GitHub issue to help the devs find bugs!}>
+ }
+ href="https://github.com/bitburner-official/bitburner-src/issues/new"
+ target="_blank"
+ sx={{ gridArea: "bug" }}
+ >
+ Report Bug
+
+
+ } onClick={() => setCreditsOpen(true)} sx={{ gridArea: "credits" }}>
+ Credits
+ setCreditsOpen(false)} />
} onClick={() => setConfirmResetOpen(true)} sx={{ gridArea: "tut" }}>
Reset tutorial