mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
ACHIEVEMENTS: Support achievements with no matching Steam achievement (#1953)
This commit is contained in:
@@ -10,6 +10,6 @@ for i in $ROOTDIR/dist/icons/achievements/*.svg; do
|
||||
echo $i
|
||||
# Make background transparent and replace green with black
|
||||
# The icons will be recolored by css filters matching the player's theme
|
||||
sed -i "s/fill:#000000;/fill-opacity: 0%;/g" "$i"
|
||||
sed -i "s/fill:#00ff00;/fill:#000000;/g" "$i"
|
||||
sed -i"" "s/fill:#000000;/fill-opacity: 0%;/g" "$i"
|
||||
sed -i"" "s/fill:#00ff00;/fill:#000000;/g" "$i"
|
||||
done
|
||||
|
||||
60
src/Achievements/AchievementCategory.tsx
Normal file
60
src/Achievements/AchievementCategory.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Typography } from "@mui/material";
|
||||
|
||||
import { Achievement } from "./Achievements";
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
achievements: { achievement: Achievement }[];
|
||||
allAchievements?: { achievement: Achievement }[];
|
||||
usePadding?: boolean;
|
||||
}
|
||||
|
||||
function steamCount(achievements: { achievement: Achievement }[]): number {
|
||||
return achievements.filter((entry) => !entry.achievement.NotInSteam).length;
|
||||
}
|
||||
|
||||
export function AchievementCategory({
|
||||
title,
|
||||
achievements,
|
||||
allAchievements,
|
||||
usePadding,
|
||||
children,
|
||||
}: React.PropsWithChildren<IProps>): JSX.Element {
|
||||
/**
|
||||
* For each achievement, we need to display the icon and the detail on the same "row" (icon on the left and detail on
|
||||
* the right). When the viewport is to small, the detail part of some achievements is "moved" to a separate "row". It
|
||||
* looks like this:
|
||||
*
|
||||
* <achievement 1>
|
||||
* <icon><detail>
|
||||
* </achievement 1>
|
||||
* <achievement 2>
|
||||
* <icon>
|
||||
* <detail>
|
||||
* </achievement 2>
|
||||
* <achievement 3>
|
||||
* <icon><detail>
|
||||
* </achievement 3>
|
||||
*
|
||||
* Using "minWidth" fixes this issue by setting a min value for the width of each row
|
||||
*/
|
||||
return (
|
||||
<Accordion defaultExpanded={!!allAchievements} disableGutters square sx={{ minWidth: "645px" }}>
|
||||
<AccordionSummary>
|
||||
{allAchievements ? (
|
||||
<Typography variant="h5" sx={{ my: 1 }}>
|
||||
{title} ({achievements.length}/{allAchievements.length}, {steamCount(achievements)}/
|
||||
{steamCount(allAchievements)} for Steam)
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="h5" color="secondary">
|
||||
{title} ({achievements.length} remaining, {steamCount(achievements)} for Steam)
|
||||
</Typography>
|
||||
)}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={usePadding ? { pt: 2 } : undefined}>{children}</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"note": "***** Generated from a script, overwritten by steam achievements data *****",
|
||||
"fetchedOn": 1641517584274,
|
||||
"note": "Originally generated by a script using Steam achievement data. Going forward, must be edited manually.",
|
||||
"achievements": {
|
||||
"CYBERSEC": {
|
||||
"ID": "CYBERSEC",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import LinkOffIcon from "@mui/icons-material/LinkOff";
|
||||
|
||||
import { Achievement } from "./Achievements";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
@@ -23,6 +24,7 @@ export function AchievementEntry({
|
||||
const isUnlocked = !!unlockedOn;
|
||||
|
||||
const mainColor = isUnlocked ? Settings.theme.primary : Settings.theme.secondarylight;
|
||||
const captionColor = isUnlocked ? Settings.theme.primarydark : Settings.theme.secondary;
|
||||
|
||||
let achievedOn = "";
|
||||
if (unlockedOn) {
|
||||
@@ -64,10 +66,25 @@ export function AchievementEntry({
|
||||
{achievement.Description}
|
||||
</Typography>
|
||||
{isUnlocked && (
|
||||
<Typography variant="caption" sx={{ fontSize: "12px", color: Settings.theme.primarydark }}>
|
||||
<Typography variant="caption" sx={{ fontSize: "12px", color: captionColor }}>
|
||||
Acquired on {achievedOn}
|
||||
</Typography>
|
||||
)}
|
||||
{achievement.NotInSteam && (
|
||||
<Box /* This box is used to vertically center the taller LinkOffIcon with the Typography */
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<LinkOffIcon sx={{ fontSize: "20px", color: captionColor, marginRight: 1 }} />
|
||||
<Typography variant="caption" sx={{ fontSize: "12px", color: captionColor }}>
|
||||
No equivalent Steam achievement
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Box, Typography } from "@mui/material";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
import { AchievementCategory } from "./AchievementCategory";
|
||||
import { AchievementEntry } from "./AchievementEntry";
|
||||
import { Achievement, PlayerAchievement } from "./Achievements";
|
||||
import { Settings } from "../Settings/Settings";
|
||||
@@ -53,80 +54,48 @@ export function AchievementList({ achievements, playerAchievements }: IProps): J
|
||||
}}
|
||||
>
|
||||
{unlocked.length > 0 && (
|
||||
<Accordion defaultExpanded disableGutters square>
|
||||
<AccordionSummary>
|
||||
<Typography variant="h5" sx={{ my: 1 }}>
|
||||
Acquired ({unlocked.length}/{data.length})
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ pt: 2 }}>
|
||||
{unlocked.map((item) => (
|
||||
<AchievementEntry
|
||||
key={`unlocked_${item.achievement.ID}`}
|
||||
achievement={item.achievement}
|
||||
unlockedOn={item.unlockedOn}
|
||||
cssFiltersUnlocked={cssPrimary}
|
||||
cssFiltersLocked={cssSecondary}
|
||||
/>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<AchievementCategory title="Acquired" achievements={unlocked} allAchievements={data} usePadding={true}>
|
||||
{unlocked.map((item) => (
|
||||
<AchievementEntry
|
||||
key={`unlocked_${item.achievement.ID}`}
|
||||
achievement={item.achievement}
|
||||
unlockedOn={item.unlockedOn}
|
||||
cssFiltersUnlocked={cssPrimary}
|
||||
cssFiltersLocked={cssSecondary}
|
||||
/>
|
||||
))}
|
||||
</AchievementCategory>
|
||||
)}
|
||||
|
||||
{locked.length > 0 && (
|
||||
<Accordion disableGutters square>
|
||||
<AccordionSummary>
|
||||
<Typography variant="h5" color="secondary">
|
||||
Locked ({locked.length} remaining)
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ pt: 2 }}>
|
||||
{locked.map((item) => (
|
||||
<AchievementEntry
|
||||
key={`locked_${item.achievement.ID}`}
|
||||
achievement={item.achievement}
|
||||
cssFiltersUnlocked={cssPrimary}
|
||||
cssFiltersLocked={cssSecondary}
|
||||
/>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<AchievementCategory title="Locked" achievements={locked} usePadding={true}>
|
||||
{locked.map((item) => (
|
||||
<AchievementEntry
|
||||
key={`locked_${item.achievement.ID}`}
|
||||
achievement={item.achievement}
|
||||
cssFiltersUnlocked={cssPrimary}
|
||||
cssFiltersLocked={cssSecondary}
|
||||
/>
|
||||
))}
|
||||
</AchievementCategory>
|
||||
)}
|
||||
|
||||
{unavailable.length > 0 && (
|
||||
<Accordion disableGutters square>
|
||||
<AccordionSummary>
|
||||
<Typography variant="h5" color="secondary">
|
||||
Unavailable ({unavailable.length} remaining)
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
{pluralize(unavailable.length, "additional achievement")} hidden behind content you don't have access
|
||||
to.
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<AchievementCategory title="Unavailable" achievements={unavailable}>
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
{pluralize(unavailable.length, "additional achievement")} hidden behind content you don't have access to.
|
||||
</Typography>
|
||||
</AchievementCategory>
|
||||
)}
|
||||
|
||||
{secret.length > 0 && (
|
||||
<Accordion disableGutters square>
|
||||
<AccordionSummary>
|
||||
<Typography variant="h5" color="secondary">
|
||||
Secret ({secret.length} remaining)
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography color="secondary" sx={{ mt: 1 }}>
|
||||
{secret.map((item) => (
|
||||
<span key={`secret_${item.achievement.ID}`}>
|
||||
<CorruptableText content={item.achievement.ID} spoiler={true}></CorruptableText>
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<AchievementCategory title="Secret" achievements={secret}>
|
||||
<Typography color="secondary" sx={{ mt: 1 }}>
|
||||
{secret.map((item) => (
|
||||
<span key={`secret_${item.achievement.ID}`}>
|
||||
<CorruptableText content={item.achievement.ID} spoiler={true}></CorruptableText>
|
||||
<br />
|
||||
</span>
|
||||
))}
|
||||
</Typography>
|
||||
</AchievementCategory>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface Achievement {
|
||||
Name?: string;
|
||||
Description?: string;
|
||||
Secret?: boolean;
|
||||
NotInSteam?: boolean;
|
||||
Condition: () => boolean;
|
||||
Visible?: () => boolean;
|
||||
AdditionalUnlock?: string[]; // IDs of achievements that should be awarded when awarding this one
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import React from "react";
|
||||
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import { AchievementList } from "./AchievementList";
|
||||
import { achievements } from "./Achievements";
|
||||
import { Typography } from "@mui/material";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Player } from "@player";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
const useStyles = makeStyles()({
|
||||
root: {
|
||||
width: 50,
|
||||
padding: theme.spacing(2),
|
||||
userSelect: "none",
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
export function AchievementsRoot(): JSX.Element {
|
||||
const { classes } = useStyles();
|
||||
return (
|
||||
<div className={classes.root} style={{ width: "90%" }}>
|
||||
<Typography variant="h4">Achievements</Typography>
|
||||
<AchievementList achievements={Object.values(achievements)} playerAchievements={Player.achievements} />
|
||||
<Box mx={2}>
|
||||
<Typography>
|
||||
Achievements are persistent rewards for various actions and challenges. A limited number of Bitburner
|
||||
achievements have corresponding achievements in Steam.
|
||||
</Typography>
|
||||
<AchievementList achievements={Object.values(achievements)} playerAchievements={Player.achievements} />
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Adding Achievements
|
||||
|
||||
- Add a .svg in `./assets/Steam/achievements/real`
|
||||
- Create the achievement in Steam Dev Portal
|
||||
- Run `sh ./assets/Steam/achievements/pack-for-web.sh`
|
||||
- Run `node ./tools/fetch-steam-achievements-data DEVKEYHERE`
|
||||
- Get your key here: https://steamcommunity.com/dev/apikey
|
||||
- Add an entry in `./src/Achievements/AchievementData.json` -> achievements
|
||||
- It should match the information for the Steam achievement, if applicable
|
||||
- Order the new achievement entry thematically
|
||||
- Add an entry in `./src/Achievements/Achievements.ts` -> achievements
|
||||
- Commit `./dist/icons/achievements` & `./src/Achievements/AchievementData.json`
|
||||
- Match the order of achievements in `AchievementData.json`
|
||||
- Commit `./dist/icons/achievements`
|
||||
|
||||
Reference in New Issue
Block a user