ACHIEVEMENTS: Support achievements with no matching Steam achievement (#1953)

This commit is contained in:
Femboy Fireball
2025-02-08 21:19:36 -06:00
committed by GitHub
parent c99fa448fa
commit 5bc9068745
8 changed files with 135 additions and 83 deletions

View File

@@ -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

View 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>
);
}

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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`