Add achievements to base game

- Add a script to generate achievement data from Steamworks API
- Add achievements page with a link in sidebar
- Calculate achievements (1/min) with an engine counter
- Store achievements with a timestamp on unlocked in the PlayerObject
- Add a script to generate monochrome icons from Steam icons
- Add toast when unlocking an achievement
This commit is contained in:
Martin Fournier 2022-01-06 07:04:03 -05:00
parent 4363aa43fe
commit 844d518684
23 changed files with 1983 additions and 416 deletions

@ -0,0 +1,15 @@
#!/bin/bash
BASEDIR=$(dirname "$0")
ROOTDIR=$BASEDIR/../../..
echo $ROOTDIR
rm -rf $ROOTDIR/dist/icons/achievements
mkdir -p $ROOTDIR/dist/icons
cp -r $BASEDIR/real $ROOTDIR/dist/icons/achievements
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"
done

@ -7,6 +7,7 @@ mkdir -p .package/node_modules || true
cp index.html .package
cp -r electron/* .package
cp -r dist/ext .package/dist
cp -r dist/icons .package/dist
# The css files
cp dist/vendor.css .package/dist

@ -0,0 +1,486 @@
{
"note": "***** Generated from a script, overwritten by steam achievements data *****",
"fetchedOn": 1641517584274,
"achievements": {
"CYBERSEC": {
"ID": "CYBERSEC",
"Name": "CyberSec",
"Description": "Join CyberSec."
},
"NITESEC": {
"ID": "NITESEC",
"Name": "avmnite-02h",
"Description": "Join NiteSec."
},
"THE_BLACK_HAND": {
"ID": "THE_BLACK_HAND",
"Name": "I.I.I.I",
"Description": "Join The Black Hand."
},
"BITRUNNERS": {
"ID": "BITRUNNERS",
"Name": "run4theh111z",
"Description": "Join the BitRunners."
},
"DAEDALUS": {
"ID": "DAEDALUS",
"Name": "fl1ght.exe",
"Description": "Join Daedalus."
},
"THE_COVENANT": {
"ID": "THE_COVENANT",
"Name": "The Covenant",
"Description": "Join The Covenant."
},
"ILLUMINATI": {
"ID": "ILLUMINATI",
"Name": "Illuminati",
"Description": "Join the Illuminati."
},
"BRUTESSH.EXE": {
"ID": "BRUTESSH.EXE",
"Name": "BruteSSH.exe",
"Description": "Acquire BruteSSH.exe"
},
"FTPCRACK.EXE": {
"ID": "FTPCRACK.EXE",
"Name": "FTPCrack.exe",
"Description": "Acquire FTPCrack.exe"
},
"RELAYSMTP.EXE": {
"ID": "RELAYSMTP.EXE",
"Name": "relaySMTP.exe",
"Description": "Acquire relaySMTP.exe"
},
"HTTPWORM.EXE": {
"ID": "HTTPWORM.EXE",
"Name": "HTTPWorm.exe",
"Description": "Acquire HTTPWorm.exe"
},
"SQLINJECT.EXE": {
"ID": "SQLINJECT.EXE",
"Name": "SQLInject.exe",
"Description": "Acquire SQLInject.exe"
},
"FORMULAS.EXE": {
"ID": "FORMULAS.EXE",
"Name": "Formulas.exe",
"Description": "Acquire Formulas.exe"
},
"SF1.1": {
"ID": "SF1.1",
"Name": "Source Genesis",
"Description": "Acquire SF1.1"
},
"SF2.1": {
"ID": "SF2.1",
"Name": "Rise of the Underworld",
"Description": "Acquire SF2.1"
},
"SF3.1": {
"ID": "SF3.1",
"Name": "Corporatocracy",
"Description": "Acquire SF3.1"
},
"SF4.1": {
"ID": "SF4.1",
"Name": "The Singularity",
"Description": "Acquire SF4.1"
},
"SF5.1": {
"ID": "SF5.1",
"Name": "Artificial Intelligence",
"Description": "Acquire SF5.1"
},
"SF6.1": {
"ID": "SF6.1",
"Name": "Bladeburners",
"Description": "Acquire SF6.1"
},
"SF7.1": {
"ID": "SF7.1",
"Name": "Bladeburners 2079",
"Description": "Acquire SF7.1"
},
"SF8.1": {
"ID": "SF8.1",
"Name": "Ghost of Wall Street",
"Description": "Acquire SF8.1"
},
"SF9.1": {
"ID": "SF9.1",
"Name": "Hacktocracy",
"Description": "Acquire SF9.1"
},
"SF10.1": {
"ID": "SF10.1",
"Name": "Digital Carbon",
"Description": "Acquire SF10.1"
},
"SF11.1": {
"ID": "SF11.1",
"Name": "The Big Crash",
"Description": "Acquire SF11.1"
},
"SF12.1": {
"ID": "SF12.1",
"Name": "The Recursion",
"Description": "Acquire SF12.1"
},
"MONEY_1Q": {
"ID": "MONEY_1Q",
"Name": "Here comes the money!",
"Description": "Have $1Q on your home computer."
},
"MONEY_M1B": {
"ID": "MONEY_M1B",
"Name": "Massive debt",
"Description": "Be $1b in debt."
},
"INSTALL_1": {
"ID": "INSTALL_1",
"Name": "I never asked for this.",
"Description": "Install your first augmentation."
},
"INSTALL_100": {
"ID": "INSTALL_100",
"Name": "I asked for this.",
"Description": "Have 100 augmentation installed at once."
},
"QUEUE_40": {
"ID": "QUEUE_40",
"Name": "It's time to install",
"Description": "Have 40 augmentation queued at once."
},
"HACKING_100000": {
"ID": "HACKING_100000",
"Name": "Power Overwhelming",
"Description": "Achieve 100 000 hacking skill."
},
"COMBAT_3000": {
"ID": "COMBAT_3000",
"Name": "One punch man",
"Description": "Achieve 3000 in all combat stats."
},
"NEUROFLUX_255": {
"ID": "NEUROFLUX_255",
"Name": "Neuroflux is love, Neuroflux is live",
"Description": "Install Neuroflux Governor level 255"
},
"NS2": {
"ID": "NS2",
"Name": "Maximum speed!",
"Description": "Write an ns2 script."
},
"FROZE": {
"ID": "FROZE",
"Name": "while(true);",
"Description": "Restart the game using the reload & kill all option because you froze it with an infinite loop."
},
"RUNNING_SCRIPTS_1000": {
"ID": "RUNNING_SCRIPTS_1000",
"Name": "Need more real life ram",
"Description": "Run 1000 scripts simultaneously."
},
"DRAIN_SERVER": {
"ID": "DRAIN_SERVER",
"Name": "Big trouble",
"Description": "Drain a server of all its money."
},
"MAX_RAM": {
"ID": "MAX_RAM",
"Name": "Download more ram",
"Description": "Maximize your home computer ram."
},
"MAX_CORES": {
"ID": "MAX_CORES",
"Name": "Download more cores?",
"Description": "Maximize your home computer cores."
},
"SCRIPTS_30": {
"ID": "SCRIPTS_30",
"Name": "Thank you folders!",
"Description": "Have 30 scripts on your home computer."
},
"KARMA_1000000": {
"ID": "KARMA_1000000",
"Name": "Wretched hive of scum and vilany",
"Description": "Reach -1m karma."
},
"STOCK_1q": {
"ID": "STOCK_1q",
"Name": "Wolf of wall stree.",
"Description": "Make 1q on the stock market."
},
"DISCOUNT": {
"ID": "DISCOUNT",
"Name": "Discount!",
"Description": "Get a discount at Powerhouse Gym by backdooring their server."
},
"SCRIPT_32GB": {
"ID": "SCRIPT_32GB",
"Name": "You'll need upgrade for this one.",
"Description": "Write a script that costs 32GB per thread."
},
"FIRST_HACKNET_NODE": {
"ID": "FIRST_HACKNET_NODE",
"Name": "Free money!",
"Description": "Purchase your first hacknet node."
},
"30_HACKNET_NODE": {
"ID": "30_HACKNET_NODE",
"Name": "Big network",
"Description": "Have 30 hacknet nodes."
},
"MAX_HACKNET_NODE": {
"ID": "MAX_HACKNET_NODE",
"Name": "That's the limit",
"Description": "Maximize a hacknet node."
},
"HACKNET_NODE_10M": {
"ID": "HACKNET_NODE_10M",
"Name": "The original hacker",
"Description": "Make 10m from hacknet nodes."
},
"REPUTATION_10M": {
"ID": "REPUTATION_10M",
"Name": "Well liked",
"Description": "Reach 10m reputation with a faction."
},
"DONATION": {
"ID": "DONATION",
"Name": "Donate!",
"Description": "Unlock donations with a faction."
},
"TRAVEL": {
"ID": "TRAVEL",
"Name": "World explorer",
"Description": "Travel anywhere."
},
"WORKOUT": {
"ID": "WORKOUT",
"Name": "Gains!",
"Description": "Workout at a gym."
},
"TOR": {
"ID": "TOR",
"Name": "The Onion Network",
"Description": "Purchase the TOR router."
},
"HOSPITALIZED": {
"ID": "HOSPITALIZED",
"Name": "Ouch!",
"Description": "Go to the hospital."
},
"GANG": {
"ID": "GANG",
"Name": "Gangster",
"Description": "Form a gang."
},
"FULL_GANG": {
"ID": "FULL_GANG",
"Name": "Don",
"Description": "Recruit all gang members."
},
"GANG_TERRITORY": {
"ID": "GANG_TERRITORY",
"Name": "Stay out of my territory",
"Description": "Have 100% of the territory."
},
"GANG_MEMBER_POWER": {
"ID": "GANG_MEMBER_POWER",
"Name": "One punch guy",
"Description": "Have a gang member with 10 000 in 1 skill."
},
"CORPORATION": {
"ID": "CORPORATION",
"Name": "A small 150b loan.",
"Description": "Create a corporation."
},
"CORPORATION_BRIBE": {
"ID": "CORPORATION_BRIBE",
"Name": "Lobbying is great!",
"Description": "Lower your taxes through lobbying."
},
"CORPORATION_PROD_1000": {
"ID": "CORPORATION_PROD_1000",
"Name": "Streamlined manufacturing",
"Description": "Have a division with a production multiplier of 1000."
},
"CORPORATION_EMPLOYEE_3000": {
"ID": "CORPORATION_EMPLOYEE_3000",
"Name": "Small town",
"Description": "Have a division with 3000 employee."
},
"CORPORATION_REAL_ESTATE": {
"ID": "CORPORATION_REAL_ESTATE",
"Name": "Own the land",
"Description": "Expand to the Real Estate division."
},
"INTELLIGENCE_255": {
"ID": "INTELLIGENCE_255",
"Name": "Smart!",
"Description": "Reach intelligence 255"
},
"BLADEBURNER_DIVISION": {
"ID": "BLADEBURNER_DIVISION",
"Name": "Bladeburners",
"Description": "Join the Bladeburner division."
},
"BLADEBURNER_OVERCLOCK": {
"ID": "BLADEBURNER_OVERCLOCK",
"Name": "Overclock!",
"Description": "Reach maximum level of Overclock"
},
"BLADEBURNER_UNSPENT_100000": {
"ID": "BLADEBURNER_UNSPENT_100000",
"Name": "You should really spent those.",
"Description": "Have 100 000 unspent bladeburner skill points."
},
"4S": {
"ID": "4S",
"Name": "4S",
"Description": "Purchase the 4S market data."
},
"FIRST_HACKNET_SERVER": {
"ID": "FIRST_HACKNET_SERVER",
"Name": "The improved hacker.",
"Description": "Purchase your first hacknet server."
},
"ALL_HACKNET_SERVER": {
"ID": "ALL_HACKNET_SERVER",
"Name": "Full network",
"Description": "Buy all hacknet servers."
},
"MAX_HACKNET_SERVER": {
"ID": "MAX_HACKNET_SERVER",
"Name": "That's the new limit.",
"Description": "Maximize a hacknet server."
},
"HACKNET_SERVER_1B": {
"ID": "HACKNET_SERVER_1B",
"Name": "Not passive anymore",
"Description": "Make $1b with hacknet servers."
},
"MAX_CACHE": {
"ID": "MAX_CACHE",
"Name": "What a waste.",
"Description": "Cap your hashes."
},
"SLEEVE_8": {
"ID": "SLEEVE_8",
"Name": "You and what army?",
"Description": "Purchase all duplicate sleeves from The Covenant."
},
"INDECISIVE": {
"ID": "INDECISIVE",
"Name": "Too many options.",
"Description": "Spend 1h straight on the bitverse."
},
"FAST_BN": {
"ID": "FAST_BN",
"Name": "Speed demon.",
"Description": "Destroy a bitnode in under 2 days."
},
"CHALLENGE_BN1": {
"ID": "CHALLENGE_BN1",
"Name": "BN1: Challenge",
"Description": "Destroy BN1 with at most 128GB and 1 core."
},
"CHALLENGE_BN2": {
"ID": "CHALLENGE_BN2",
"Name": "BN2: Challenge",
"Description": "Destroy BN2 without forming a gang."
},
"CHALLENGE_BN3": {
"ID": "CHALLENGE_BN3",
"Name": "BN3: Challenge",
"Description": "Destroy BN3 without creating corporation."
},
"CHALLENGE_BN6": {
"ID": "CHALLENGE_BN6",
"Name": "BN6: Challenge",
"Description": "Destroy BN6 without joining the bladeburner division."
},
"CHALLENGE_BN7": {
"ID": "CHALLENGE_BN7",
"Name": "BN7: Challenge",
"Description": "Destroy BN7 without joining the bladeburner division."
},
"CHALLENGE_BN8": {
"ID": "CHALLENGE_BN8",
"Name": "BN8: Challenge",
"Description": "Destroy BN8 without purchasing the 4s market data."
},
"CHALLENGE_BN9": {
"ID": "CHALLENGE_BN9",
"Name": "BN9: Challenge",
"Description": "Destroy BN9 without using hacknet servers."
},
"CHALLENGE_BN10": {
"ID": "CHALLENGE_BN10",
"Name": "BN10: Challenge",
"Description": "Destroy BN10 without using sleeves."
},
"CHALLENGE_BN12": {
"ID": "CHALLENGE_BN12",
"Name": "BN12: Challenge",
"Description": "Destroy BN12 50 times."
},
"BYPASS": {
"ID": "BYPASS",
"Name": "Exploit: bypass",
"Description": "Circumventing the ram cost of document."
},
"PROTOTYPETAMPERING": {
"ID": "PROTOTYPETAMPERING",
"Name": "Exploit: prototype tampering",
"Description": "Tamper with the Numbers prototype."
},
"UNCLICKABLE": {
"ID": "UNCLICKABLE",
"Name": "Exploit: unclickable",
"Description": "Click the unclickable."
},
"UNDOCUMENTEDFUNCTIONCALL": {
"ID": "UNDOCUMENTEDFUNCTIONCALL",
"Name": "Exploit: undocumented",
"Description": "Call the undocumented function."
},
"TIMECOMPRESSION": {
"ID": "TIMECOMPRESSION",
"Name": "Exploit: time compression",
"Description": "Compress time."
},
"REALITYALTERATION": {
"ID": "REALITYALTERATION",
"Name": "Exploit: reality alteration",
"Description": "Alter reality."
},
"N00DLES": {
"ID": "N00DLES",
"Name": "Exploit: noodles",
"Description": "Harness the power of the noodles."
},
"EDITSAVEFILE": {
"ID": "EDITSAVEFILE",
"Name": "Exploit: edit",
"Description": "Acquire the EditSaveFile Source-File -1"
},
"UNACHIEVABLE": {
"ID": "UNACHIEVABLE",
"Name": "UNACHIEVABLE",
"Description": "This achievement cannot be unlocked."
},
"CHALLENGE_BN13": {
"ID": "CHALLENGE_BN13",
"Name": "BN13: Challenge",
"Description": "Complete BN13 without Stanek's Gift."
},
"DEVMENU": {
"ID": "DEVMENU",
"Name": "Exploit: edit",
"Description": "Open the dev menu."
}
}
}

@ -0,0 +1,60 @@
import React from "react";
import { Box, Typography } from "@mui/material";
import { Achievement } from "./Achievements";
import { Settings } from "../Settings/Settings"
import { AchievementIcon } from "./AchievementIcon";
interface IProps {
achievement: Achievement;
unlockedOn?: number;
cssFiltersUnlocked: string;
cssFiltersLocked: string;
}
export function AchievementEntry({ achievement, unlockedOn, cssFiltersUnlocked, cssFiltersLocked }: IProps): JSX.Element {
if (!achievement) return <></>;
const isUnlocked = !!unlockedOn;
const mainColor = isUnlocked ? Settings.theme.primary : Settings.theme.secondarylight;
let achievedOn = '';
if (unlockedOn) {
achievedOn = new Date(unlockedOn).toLocaleString();
}
return (
<Box sx={{
border: `1px solid ${Settings.theme.well}`, mb: 2
}}>
<Box sx={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
}}>
<AchievementIcon
achievement={achievement} unlocked={isUnlocked} size="72px"
colorFilters={isUnlocked ? cssFiltersUnlocked: cssFiltersLocked} />
<Box sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 1
}}>
<Typography variant="h6" sx={{ color: mainColor}}>
{achievement.Name}
</Typography>
<Typography variant="body2" sx={{ maxWidth: '500px', color: mainColor}}>
{achievement.Description}
</Typography>
{isUnlocked && (
<Typography variant="caption" sx={{ fontSize: '12px', color: Settings.theme.primarydark }}>
Acquired on {achievedOn}
</Typography>
)}
</Box>
</Box>
</Box>
);
}

@ -0,0 +1,35 @@
import React, { useState } from "react";
import { Box } from "@mui/material";
import { Achievement } from "./Achievements";
import { Settings } from "../Settings/Settings"
interface IProps {
achievement: Achievement;
unlocked: boolean;
colorFilters: string;
size: string;
}
export function AchievementIcon({ achievement, unlocked, colorFilters, size }: IProps): JSX.Element {
const [imgLoaded, setImgLoaded] = useState(false);
const mainColor = unlocked ? Settings.theme.primarydark : Settings.theme.secondarydark;
if (!achievement.Icon) return (<></>);
return (
<Box
sx={{
border: `1px solid ${mainColor}`,
width: size, height: size,
m: 1,
visibility: imgLoaded ? 'visible' : 'hidden'
}}
>
<img src={`dist/icons/achievements/${encodeURI(achievement.Icon)}.svg`}
style={{ filter: colorFilters, width: size, height: size }}
onLoad={() => setImgLoaded(true)}
alt={achievement.Name} />
</Box>
);
}

@ -0,0 +1,121 @@
import React from "react";
import { Accordion, AccordionSummary, AccordionDetails, Box, Typography } from "@mui/material";
import { AchievementEntry } from "./AchievementEntry";
import { Achievement, PlayerAchievement} from "./Achievements";
import { Settings } from "../Settings/Settings"
import { getFiltersFromHex } from "../ThirdParty/colorUtils";
import { CorruptableText } from "../ui/React/CorruptableText";
interface IProps {
achievements: Achievement[];
playerAchievements: PlayerAchievement[];
}
export function AchievementList({ achievements, playerAchievements }: IProps): JSX.Element {
// Need to transform the primary color into css filters to change the color of the SVG.
const cssPrimary = getFiltersFromHex(Settings.theme.primary);
const cssSecondary = getFiltersFromHex(Settings.theme.secondary);
const data = achievements.map(achievement => ({
achievement,
unlockedOn: playerAchievements.find(playerAchievement => playerAchievement.ID === achievement.ID)?.unlockedOn,
})).sort((a, b) => (b.unlockedOn ?? 0) - (a.unlockedOn ?? 0));
const unlocked = data.filter(entry => entry.unlockedOn);
// Hidden achievements
const secret = data.filter(entry => !entry.unlockedOn && entry.achievement.Secret)
// Locked behind locked content (bitnode x)
const unavailable = data.filter(entry => !entry.unlockedOn && !entry.achievement.Secret && entry.achievement.Visible && entry.achievement.Visible());
// Remaining achievements
const locked = data
.filter(entry => !unlocked.map(u => u.achievement.ID).includes(entry.achievement.ID))
.filter(entry => !secret.map(u => u.achievement.ID).includes(entry.achievement.ID))
.filter(entry => !unavailable.map(u => u.achievement.ID).includes(entry.achievement.ID));
return (
<Box sx={{ pr: 18, my: 2 }}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
}}>
{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={item.achievement.ID}
achievement={item.achievement}
unlockedOn={item.unlockedOn}
cssFiltersUnlocked={cssPrimary}
cssFiltersLocked={cssSecondary} />
))}
</AccordionDetails>
</Accordion>
)}
{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={item.achievement.ID}
achievement={item.achievement}
cssFiltersUnlocked={cssPrimary}
cssFiltersLocked={cssSecondary} />
))}
</AccordionDetails>
</Accordion>
)}
{unavailable.length > 0 && (
<Accordion disableGutters square>
<AccordionSummary>
<Typography variant="h5" color="secondary">
Unavailable ({unavailable.length} remaining)
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography sx={{ mt: 1 }}>
{unavailable.length} additional achievements hidden behind content you don't have access to.
</Typography>
</AccordionDetails>
</Accordion>
)}
{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 => (
<>
<CorruptableText content={item.achievement.ID}></CorruptableText>
<br />
</>
))}
</Typography>
</AccordionDetails>
</Accordion>
)}
</Box>
</Box>
);
}

@ -0,0 +1,768 @@
import { PlayerObject } from "src/PersonObjects/Player/PlayerObject";
import { AugmentationNames } from "../Augmentation/data/AugmentationNames";
import { SkillNames } from "../Bladeburner/data/SkillNames";
import { Skills } from "../Bladeburner/Skills";
import { CONSTANTS } from "../Constants";
import { Industries } from "../Corporation/IndustryData";
import { Exploit } from "../Exploits/Exploit";
import { Factions } from "../Faction/Factions";
import { AllGangs } from "../Gang/AllGangs";
import { GangConstants } from "../Gang/data/Constants";
import { HacknetNodeConstants, HacknetServerConstants } from "../Hacknet/data/Constants";
import { hasHacknetServers } from "../Hacknet/HacknetHelpers";
import { HacknetNode } from "../Hacknet/HacknetNode";
import { HacknetServer } from "../Hacknet/HacknetServer";
import { CityName } from "../Locations/data/CityNames";
import { Player } from "../Player";
import { Programs } from "../Programs/Programs";
import { GetAllServers, GetServer } from "../Server/AllServers";
import { SpecialServers } from "../Server/data/SpecialServers";
import { Server } from "../Server/Server";
import { Router } from "../ui/GameRoot";
import { Page } from "../ui/Router";
import { IMap } from '../types';
import * as data from "./AchievementData.json";
// Unable to correctly cast the JSON data into AchievementDataJson type otherwise...
const achievementData = (<AchievementDataJson><unknown>data).achievements;
export interface Achievement {
ID: string;
Icon?: string;
Name?: string;
Description?: string;
Secret?: boolean;
Condition: () => boolean;
Visible?: () => boolean;
}
export interface PlayerAchievement {
ID: string;
unlockedOn?: number;
}
export interface AchievementDataJson {
achievements: IMap<AchievementData>;
}
export interface AchievementData {
ID: string;
Name: string;
Description: string;
}
function bitNodeFinishedState(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) return false;
if (wd.backdoorInstalled) return true;
return Player.bladeburner !== null && Player.bladeburner.blackops.hasOwnProperty("Operation Daedalus");
}
function hasAccessToSF(player: PlayerObject, bn: number): boolean {
return player.bitNodeN === bn || player.sourceFiles.some((a) => a.n === bn);
}
function knowsAboutBitverse(player: PlayerObject): boolean {
return player.sourceFiles.some((a) => a.n === 1)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function sfAchievement(): Achievement[] {
const achs: Achievement[] = [];
for (let i = 0; i <= 11; i++) {
for (let j = 1; j <= 3; j++) {
achs.push({
ID: `SF${i}.${j}`,
Condition: () => Player.sourceFileLvl(i) >= j,
});
}
}
return achs;
}
export const achievements: IMap<Achievement> = {
"CYBERSEC": {
...achievementData['CYBERSEC'],
Icon: "CSEC",
Condition: () => Player.factions.includes("CyberSec"),
},
"NITESEC": {
...achievementData['NITESEC'],
Icon: "NiteSec",
Condition: () => Player.factions.includes("NiteSec"),
},
"THE_BLACK_HAND": {
...achievementData['THE_BLACK_HAND'],
Icon: "TBH",
Condition: () => Player.factions.includes("The Black Hand"),
},
"BITRUNNERS": {
...achievementData['BITRUNNERS'],
Icon: 'bitrunners',
Condition: () => Player.factions.includes("BitRunners"),
},
"DAEDALUS": {
...achievementData['DAEDALUS'],
Icon: "daedalus",
Condition: () => Player.factions.includes("Daedalus"),
},
"THE_COVENANT": {
...achievementData['THE_COVENANT'],
Icon: "thecovenant",
Condition: () => Player.factions.includes("The Covenant"),
},
"ILLUMINATI": {
...achievementData['ILLUMINATI'],
Icon: 'illuminati',
Condition: () => Player.factions.includes("Illuminati") ,
},
"BRUTESSH.EXE": {
...achievementData['BRUTESSH.EXE'],
Icon: 'p0',
Condition: () => Player.getHomeComputer().programs.includes(Programs.BruteSSHProgram.name),
},
"FTPCRACK.EXE": {
...achievementData['FTPCRACK.EXE'],
Icon: 'p1',
Condition: () => Player.getHomeComputer().programs.includes(Programs.FTPCrackProgram.name),
},
//-----------------------------------------------------
"RELAYSMTP.EXE": {
...achievementData['RELAYSMTP.EXE'],
Icon: 'p2',
Condition: () => Player.getHomeComputer().programs.includes(Programs.RelaySMTPProgram.name),
},
"HTTPWORM.EXE": {
...achievementData['HTTPWORM.EXE'],
Icon: 'p3',
Condition: () => Player.getHomeComputer().programs.includes(Programs.HTTPWormProgram.name),
},
"SQLINJECT.EXE": {
...achievementData['SQLINJECT.EXE'],
Icon: 'p4',
Condition: () => Player.getHomeComputer().programs.includes(Programs.SQLInjectProgram.name),
},
"FORMULAS.EXE": {
...achievementData['FORMULAS.EXE'],
Icon: 'formulas',
Condition: () => Player.getHomeComputer().programs.includes(Programs.Formulas.name),
},
"SF1.1": {
...achievementData['SF1.1'],
Icon: 'SF1.1',
Visible: () => hasAccessToSF(Player, 1),
Condition: () => Player.sourceFileLvl(1) >= 1
},
"SF2.1": {
...achievementData['SF2.1'],
Icon: "SF2.1",
Visible: () => hasAccessToSF(Player, 2),
Condition: () => Player.sourceFileLvl(2) >= 1,
},
"SF3.1": {
...achievementData['SF3.1'],
Icon: "SF3.1",
Visible: () => hasAccessToSF(Player, 3),
Condition: () => Player.sourceFileLvl(3) >= 1,
},
"SF4.1": {
...achievementData['SF4.1'],
Icon: "SF4.1",
Visible: () => hasAccessToSF(Player, 4),
Condition: () => Player.sourceFileLvl(4) >= 1,
},
"SF5.1": {
...achievementData['SF5.1'],
Icon: "SF5.1",
Visible: () => hasAccessToSF(Player, 5),
Condition: () => Player.sourceFileLvl(5) >= 1,
},
"SF6.1": {
...achievementData['SF6.1'],
Icon: "SF6.1",
Visible: () => hasAccessToSF(Player, 6),
Condition: () => Player.sourceFileLvl(6) >= 1,
},
"SF7.1": {
...achievementData['SF7.1'],
Icon: "SF7.1",
Visible: () => hasAccessToSF(Player, 7),
Condition: () => Player.sourceFileLvl(7) >= 1,
},
"SF8.1": {
...achievementData['SF8.1'],
Icon: "SF8.1",
Visible: () => hasAccessToSF(Player, 8),
Condition: () => Player.sourceFileLvl(8) >= 1,
},
"SF9.1": {
...achievementData['SF9.1'],
Icon: "SF9.1",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => Player.sourceFileLvl(9) >= 1,
},
"SF10.1": {
...achievementData['SF10.1'],
Icon: "SF10.1",
Visible: () => hasAccessToSF(Player, 10),
Condition: () => Player.sourceFileLvl(10) >= 1
},
"SF11.1": {
...achievementData['SF11.1'],
Icon: "SF11.1",
Visible: () => hasAccessToSF(Player, 11),
Condition: () => Player.sourceFileLvl(11) >= 1
},
"SF12.1": {
...achievementData['SF12.1'],
Icon: "SF12.1",
Visible: () => hasAccessToSF(Player, 12),
Condition: () => Player.sourceFileLvl(12) >= 1
},
"MONEY_1Q": {
...achievementData['MONEY_1Q'],
Icon: "$1Q",
Condition: () => Player.money >= 1e18,
},
"MONEY_M1B": {
...achievementData['MONEY_M1B'],
Icon: "-1b",
Secret: true,
Condition: () => Player.money <= -1e9,
},
"INSTALL_1": {
...achievementData['INSTALL_1'],
Icon: "install",
Condition: () => Player.augmentations.length >= 1,
},
"INSTALL_100": {
...achievementData['INSTALL_100'],
Icon: "install_100",
Condition: () => Player.augmentations.length >= 100,
},
"QUEUE_40": {
...achievementData['QUEUE_40'],
Icon: "queue40",
Condition: () => Player.queuedAugmentations.length >= 40,
},
"HACKING_100000": {
...achievementData['HACKING_100000'],
Icon: "hack100000",
Condition: () => Player.hacking >= 100000,
},
"COMBAT_3000": {
...achievementData['COMBAT_3000'],
Icon: "combat3000",
Condition: () =>
Player.strength >= 3000 && Player.defense >= 3000 && Player.dexterity >= 3000 && Player.agility >= 3000,
},
"NEUROFLUX_255": {
...achievementData['NEUROFLUX_255'],
Icon: "nf255",
Condition: () => Player.augmentations.some((a) => a.name === AugmentationNames.NeuroFluxGovernor && a.level >= 255),
},
"NS2": {
...achievementData['NS2'],
Icon: "ns2",
Condition: () => Player.getHomeComputer().scripts.some((s) => s.filename.endsWith(".js") || s.filename.endsWith(".ns")),
},
"FROZE": {
...achievementData['FROZE'],
Icon: "forze",
Condition: () => location.href.includes("noScripts")
},
"RUNNING_SCRIPTS_1000": {
...achievementData['RUNNING_SCRIPTS_1000'],
Icon: "run1000",
Condition: (): boolean => {
let running = 0;
for (const s of GetAllServers()) {
running += s.runningScripts.length;
}
return running >= 1000;
},
},
"DRAIN_SERVER": {
...achievementData['DRAIN_SERVER'],
Icon: "drain",
Condition: (): boolean => {
for (const s of GetAllServers()) {
if (s instanceof Server) {
if (s.moneyMax > 0 && s.moneyAvailable === 0) return true;
}
}
return false;
},
},
"MAX_RAM": {
...achievementData['MAX_RAM'],
Icon: "maxram",
Condition: () => Player.getHomeComputer().maxRam === CONSTANTS.HomeComputerMaxRam
},
"MAX_CORES": {
...achievementData['MAX_CORES'],
Icon: "maxcores",
Condition: () => Player.getHomeComputer().cpuCores === 8
},
"SCRIPTS_30": {
...achievementData['SCRIPTS_30'],
Icon: "folders",
Condition: () => Player.getHomeComputer().scripts.length >= 30
},
"KARMA_1000000": {
...achievementData['KARMA_1000000'],
Icon: "karma",
Secret: true,
Condition: () => Player.karma <= -1e6
},
"STOCK_1q": {
...achievementData['STOCK_1q'],
Icon: "$1Q",
Condition: () => Player.moneySourceB.stock >= 1e15
},
"DISCOUNT": {
...achievementData['DISCOUNT'],
Icon: "discount",
Condition: (): boolean => {
const p = GetServer("powerhouse-fitness");
if (!(p instanceof Server)) return false;
return p.backdoorInstalled;
},
},
"SCRIPT_32GB": {
...achievementData['SCRIPT_32GB'],
Icon: "bigcost",
Condition: () => Player.getHomeComputer().scripts.some((s) => s.ramUsage >= 32),
},
"FIRST_HACKNET_NODE": {
...achievementData['FIRST_HACKNET_NODE'],
Condition: () => !hasHacknetServers(Player) && Player.hacknetNodes.length > 0,
},
"30_HACKNET_NODE": {
...achievementData['30_HACKNET_NODE'],
Icon: "hacknet-all",
Condition: () => !hasHacknetServers(Player) && Player.hacknetNodes.length >= 30,
},
"MAX_HACKNET_NODE": {
...achievementData['MAX_HACKNET_NODE'],
Icon: "hacknet-max",
Condition: (): boolean => {
if (hasHacknetServers(Player)) return false;
for (const h of Player.hacknetNodes) {
if (!(h instanceof HacknetNode)) return false;
if (
h.ram === HacknetNodeConstants.MaxRam &&
h.cores === HacknetNodeConstants.MaxCores &&
h.level === HacknetNodeConstants.MaxLevel
)
return true;
}
return false;
},
},
"HACKNET_NODE_10M": {
...achievementData['HACKNET_NODE_10M'],
Icon: "hacknet-10m",
Condition: () => !hasHacknetServers(Player) && Player.moneySourceB.hacknet >= 10e6,
},
"REPUTATION_10M": {
...achievementData['REPUTATION_10M'],
Icon: "reputation",
Condition: () => Object.values(Factions).some((f) => f.playerReputation >= 10e6),
},
"DONATION": {
...achievementData['DONATION'],
Icon: "donation",
Condition: () => Object.values(Factions).some((f) => f.favor >= 150),
},
"TRAVEL": {
...achievementData['TRAVEL'],
Icon: "travel",
Condition: () => Player.city !== CityName.Sector12,
},
"WORKOUT": {
...achievementData['WORKOUT'],
Icon: "WORKOUT",
Condition: () =>
[
CONSTANTS.ClassGymStrength,
CONSTANTS.ClassGymDefense,
CONSTANTS.ClassGymDexterity,
CONSTANTS.ClassGymAgility,
].includes(Player.className),
},
"TOR": {
...achievementData['TOR'],
Icon: "TOR",
Condition: () => Player.hasTorRouter(),
},
"HOSPITALIZED": {
...achievementData['HOSPITALIZED'],
Icon: "OUCH",
Condition: () => Player.moneySourceB.hospitalization !== 0,
},
"GANG": {
...achievementData['GANG'],
Icon: "GANG",
Visible: () => hasAccessToSF(Player, 2),
Condition: () => Player.gang !== null,
},
"FULL_GANG": {
...achievementData['FULL_GANG'],
Icon: "GANGMAX",
Visible: () => hasAccessToSF(Player, 2),
Condition: () => Player.gang !== null && Player.gang.members.length === GangConstants.MaximumGangMembers,
},
"GANG_TERRITORY": {
...achievementData['GANG_TERRITORY'],
Icon: "GANG100%",
Visible: () => hasAccessToSF(Player, 2),
Condition: () => Player.gang !== null && AllGangs[Player.gang.facName].territory >= 0.999,
},
"GANG_MEMBER_POWER": {
...achievementData['GANG_MEMBER_POWER'],
Icon: "GANG10000",
Visible: () => hasAccessToSF(Player, 2),
Condition: () =>
Player.gang !== null &&
Player.gang.members.some((m) => m.hack >= 10000 || m.str >= 10000 || m.def >= 10000 || m.dex >= 10000 || m.agi >= 10000 || m.cha >= 10000),
},
"CORPORATION": {
...achievementData['CORPORATION'],
Icon: "CORP",
Visible: () => hasAccessToSF(Player, 3),
Condition: () => Player.corporation !== null,
},
"CORPORATION_BRIBE": {
...achievementData['CORPORATION_BRIBE'],
Icon: "CORPLOBBY",
Visible: () => hasAccessToSF(Player, 3),
Condition: () => Player.corporation !== null && Player.corporation.unlockUpgrades[6] === 1,
},
"CORPORATION_PROD_1000": {
...achievementData['CORPORATION_PROD_1000'],
Icon: "CORP1000",
Visible: () => hasAccessToSF(Player, 3),
Condition: () => Player.corporation !== null && Player.corporation.divisions.some((d) => d.prodMult >= 1000),
},
"CORPORATION_EMPLOYEE_3000": {
...achievementData['CORPORATION_EMPLOYEE_3000'],
Icon: "CORPCITY",
Visible: () => hasAccessToSF(Player, 3),
Condition: (): boolean => {
if (Player.corporation === null) return false;
for (const d of Player.corporation.divisions) {
for (const o of Object.values(d.offices)) {
if (o === 0) continue;
if (o.employees.length > 3000) return true;
}
}
return false;
},
},
"CORPORATION_REAL_ESTATE": {
...achievementData['CORPORATION_REAL_ESTATE'],
Icon: "CORPRE",
Name: "Own the land",
Description: "Expand to the Real Estate division.",
Visible: () => hasAccessToSF(Player, 3),
Condition: () => Player.corporation !== null && Player.corporation.divisions.some((d) => d.type === Industries.RealEstate),
},
"INTELLIGENCE_255": {
...achievementData['INTELLIGENCE_255'],
Icon: "INT255",
Visible: () => hasAccessToSF(Player, 5),
Condition: () => Player.intelligence >= 255,
},
"BLADEBURNER_DIVISION": {
...achievementData['BLADEBURNER_DIVISION'],
Icon: "BLADE",
Visible: () => hasAccessToSF(Player, 6),
Condition: () => Player.bladeburner !== null,
},
"BLADEBURNER_OVERCLOCK": {
...achievementData['BLADEBURNER_OVERCLOCK'],
Icon: "BLADEOVERCLOCK",
Visible: () => hasAccessToSF(Player, 6),
Condition: () =>
Player.bladeburner !== null &&
Player.bladeburner.skills[SkillNames.Overclock] === Skills[SkillNames.Overclock].maxLvl,
},
"BLADEBURNER_UNSPENT_100000": {
...achievementData['BLADEBURNER_UNSPENT_100000'],
Icon: "BLADE100K",
Visible: () => hasAccessToSF(Player, 6),
Condition: () => Player.bladeburner !== null && Player.bladeburner.skillPoints >= 100000,
},
"4S": {
...achievementData['4S'],
Icon: "4S",
Condition: () => Player.has4SData
},
"FIRST_HACKNET_SERVER": {
...achievementData['FIRST_HACKNET_SERVER'],
Icon: "HASHNET",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.hacknetNodes.length > 0,
},
"ALL_HACKNET_SERVER": {
...achievementData['ALL_HACKNET_SERVER'],
Icon: "HASHNETALL",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.hacknetNodes.length === HacknetServerConstants.MaxServers,
},
"MAX_HACKNET_SERVER": {
...achievementData['MAX_HACKNET_SERVER'],
Icon: "HASHNETALL",
Visible: () => hasAccessToSF(Player, 9),
Condition: (): boolean => {
if (!hasHacknetServers(Player)) return false;
for (const h of Player.hacknetNodes) {
if (typeof h !== "string") return false;
const hs = GetServer(h);
if (!(hs instanceof HacknetServer)) return false;
if (
hs.maxRam === HacknetServerConstants.MaxRam &&
hs.cores === HacknetServerConstants.MaxCores &&
hs.level === HacknetServerConstants.MaxLevel &&
hs.cache === HacknetServerConstants.MaxCache
)
return true;
}
return false;
},
},
"HACKNET_SERVER_1B": {
...achievementData['HACKNET_SERVER_1B'],
Icon: "HASHNETMONEY",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.moneySourceB.hacknet >= 1e9,
},
"MAX_CACHE": {
...achievementData['MAX_CACHE'],
Icon: "HASHNETCAP",
Visible: () => hasAccessToSF(Player, 9),
Condition: () => hasHacknetServers(Player) && Player.hashManager.hashes === Player.hashManager.capacity,
},
"SLEEVE_8": {
...achievementData['SLEEVE_8'],
Icon: "SLEEVE8",
Visible: () => hasAccessToSF(Player, 10),
Condition: () => Player.sleeves.length === 8,
},
"INDECISIVE": {
...achievementData['INDECISIVE'],
Icon: "1H",
Visible: () => knowsAboutBitverse(Player),
Condition: (function () {
let c = 0;
setInterval(() => {
if (Router.page() === Page.BitVerse) {
c++;
} else {
c = 0;
}
}, 60 * 1000);
return () => c > 60;
})(),
},
"FAST_BN": {
...achievementData['FAST_BN'],
Icon: "2DAYS",
Visible: () => knowsAboutBitverse(Player),
Condition: () => bitNodeFinishedState() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2,
},
"CHALLENGE_BN1": {
...achievementData['CHALLENGE_BN1'],
Icon: "BN1+",
Visible: () => knowsAboutBitverse(Player),
Condition: () =>
Player.bitNodeN === 1 &&
bitNodeFinishedState() &&
Player.getHomeComputer().maxRam <= 128 &&
Player.getHomeComputer().cpuCores === 1,
},
"CHALLENGE_BN2": {
...achievementData['CHALLENGE_BN2'],
Icon: "BN2+",
Visible: () => hasAccessToSF(Player, 2),
Condition: () => Player.bitNodeN === 2 && bitNodeFinishedState() && Player.gang === null,
},
"CHALLENGE_BN3": {
...achievementData['CHALLENGE_BN3'],
Icon: "BN3+",
Visible: () => hasAccessToSF(Player, 3),
Condition: () => Player.bitNodeN === 3 && bitNodeFinishedState() && Player.corporation === null,
},
"CHALLENGE_BN6": {
...achievementData['CHALLENGE_BN6'],
Icon: "BN6+",
Visible: () => hasAccessToSF(Player, 6),
Condition: () => Player.bitNodeN === 6 && bitNodeFinishedState() && Player.bladeburner === null,
},
"CHALLENGE_BN7": {
...achievementData['CHALLENGE_BN7'],
Icon: "BN7+",
Visible: () => hasAccessToSF(Player, 7),
Condition: () => Player.bitNodeN === 7 && bitNodeFinishedState() && Player.bladeburner === null,
},
"CHALLENGE_BN8": {
...achievementData['CHALLENGE_BN8'],
Icon: "BN8+",
Visible: () => hasAccessToSF(Player, 8),
Condition: () => Player.bitNodeN === 8 && bitNodeFinishedState() && !Player.has4SData && !Player.has4SDataTixApi,
},
"CHALLENGE_BN9": {
...achievementData['CHALLENGE_BN9'],
Icon: "BN9+",
Visible: () => hasAccessToSF(Player, 9),
Condition: () =>
Player.bitNodeN === 9 &&
bitNodeFinishedState() &&
Player.moneySourceB.hacknet === 0 &&
Player.moneySourceB.hacknet_expenses === 0,
},
"CHALLENGE_BN10": {
...achievementData['CHALLENGE_BN10'],
Icon: "BN10+",
Visible: () => hasAccessToSF(Player, 10),
Condition: () =>
Player.bitNodeN === 10 &&
bitNodeFinishedState() &&
!Player.sleeves.some(
(s) =>
s.augmentations.length > 0 ||
s.hacking_exp > 0 ||
s.strength_exp > 0 ||
s.defense_exp > 0 ||
s.agility_exp > 0 ||
s.dexterity_exp > 0 ||
s.charisma_exp > 0,
),
},
"CHALLENGE_BN12": {
...achievementData['CHALLENGE_BN12'],
Icon: "BN12+",
Visible: () => hasAccessToSF(Player, 12),
Condition: () => Player.sourceFileLvl(12) >= 50
},
"BYPASS": {
...achievementData['BYPASS'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.Bypass)
},
"PROTOTYPETAMPERING": {
...achievementData['PROTOTYPETAMPERING'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.PrototypeTampering)
},
"UNCLICKABLE": {
...achievementData['UNCLICKABLE'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.Unclickable)
},
"UNDOCUMENTEDFUNCTIONCALL": {
...achievementData['UNDOCUMENTEDFUNCTIONCALL'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.UndocumentedFunctionCall)
},
"TIMECOMPRESSION": {
...achievementData['TIMECOMPRESSION'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.TimeCompression)
},
"REALITYALTERATION": {
...achievementData['REALITYALTERATION'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.RealityAlteration)
},
"N00DLES": {
...achievementData['N00DLES'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.N00dles)
},
"EDITSAVEFILE": {
...achievementData['EDITSAVEFILE'],
Icon: "SF-1",
Secret: true,
Condition: () => Player.exploits.includes(Exploit.EditSaveFile)
},
"UNACHIEVABLE": {
...achievementData['UNACHIEVABLE'],
Icon: "SF-1",
Secret: true,
// Hey Players! Yes, you're supposed to modify this to get the achievement!
Condition: () => false,
},
"CHALLENGE_BN13": {
...achievementData['CHALLENGE_BN13'],
Icon: "BN13+",
Visible: () => hasAccessToSF(Player, 13),
Condition: () =>
Player.bitNodeN === 13 &&
bitNodeFinishedState() &&
!Player.augmentations.some((a) => a.name === AugmentationNames.StaneksGift1),
},
"DEVMENU": {
...achievementData['DEVMENU'],
Icon: "SF-1",
Condition: () => Player.exploits.includes(Exploit.YoureNotMeantToAccessThis)
}
}
// Steam has a limit of 100 achievement. So these were planned but commented for now.
// { ID: "ECORP", Condition: () => Player.factions.includes("ECorp") },
// { ID: "MEGACORP", Condition: () => Player.factions.includes("MegaCorp") },
// { ID: "BACHMAN_&_ASSOCIATES", Condition: () => Player.factions.includes("Bachman & Associates") },
// { ID: "BLADE_INDUSTRIES", Condition: () => Player.factions.includes("Blade Industries") },
// { ID: "NWO", Condition: () => Player.factions.includes("NWO") },
// { ID: "CLARKE_INCORPORATED", Condition: () => Player.factions.includes("Clarke Incorporated") },
// { ID: "OMNITEK_INCORPORATED", Condition: () => Player.factions.includes("OmniTek Incorporated") },
// { ID: "FOUR_SIGMA", Condition: () => Player.factions.includes("Four Sigma") },
// { ID: "KUAIGONG_INTERNATIONAL", Condition: () => Player.factions.includes("KuaiGong International") },
// { ID: "FULCRUM_SECRET_TECHNOLOGIES", Condition: () => Player.factions.includes("Fulcrum Secret Technologies") },
// { ID: "AEVUM", Condition: () => Player.factions.includes("Aevum") },
// { ID: "CHONGQING", Condition: () => Player.factions.includes("Chongqing") },
// { ID: "ISHIMA", Condition: () => Player.factions.includes("Ishima") },
// { ID: "NEW_TOKYO", Condition: () => Player.factions.includes("New Tokyo") },
// { ID: "SECTOR-12", Condition: () => Player.factions.includes("Sector-12") },
// { ID: "VOLHAVEN", Condition: () => Player.factions.includes("Volhaven") },
// { ID: "SPEAKERS_FOR_THE_DEAD", Condition: () => Player.factions.includes("Speakers for the Dead") },
// { ID: "THE_DARK_ARMY", Condition: () => Player.factions.includes("The Dark Army") },
// { ID: "THE_SYNDICATE", Condition: () => Player.factions.includes("The Syndicate") },
// { ID: "SILHOUETTE", Condition: () => Player.factions.includes("Silhouette") },
// { ID: "TETRADS", Condition: () => Player.factions.includes("Tetrads") },
// { ID: "SLUM_SNAKES", Condition: () => Player.factions.includes("Slum Snakes") },
// { ID: "NETBURNERS", Condition: () => Player.factions.includes("Netburners") },
// { ID: "TIAN_DI_HUI", Condition: () => Player.factions.includes("Tian Di Hui") },
// { ID: "BLADEBURNERS", Condition: () => Player.factions.includes("Bladeburners") },
// { ID: "DEEPSCANV1.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.DeepscanV1.name) },
// { ID: "DEEPSCANV2.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.DeepscanV2.name) },
// {
// ID: "SERVERPROFILER.EXE",
// Condition: () => Player.getHomeComputer().programs.includes(Programs.ServerProfiler.name),
// },
// { ID: "AUTOLINK.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.AutoLink.name) },
// { ID: "FLIGHT.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.Flight.name) },
export function calculateAchievements(): void {
const availableAchievements = Object.values(achievements).filter((a) => a.Condition()).map((a) => a.ID);
const playerAchievements = Player.achievements.map((a) => a.ID);
const newAchievements = availableAchievements.filter(a => !playerAchievements.includes(a));
for (const id of newAchievements) {
Player.giveAchievement(id);
}
// Write all player's achievements to document for Steam/Electron
// This could be replaced by "availableAchievements"
// if we don't want to grant the save game achievements to steam but only currently available
(document as any).achievements = [...Player.achievements.map(a => a.ID)];
}

@ -0,0 +1,30 @@
import React from "react";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
import { Theme } from "@mui/material/styles";
import { AchievementList } from "./AchievementList";
import { achievements } from "./Achievements";
import { Typography } from "@mui/material";
import { Player } from "../Player";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
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} />
</div>
);
}

@ -0,0 +1,9 @@
# 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/Achievements.ts` -> achievements
* Commit `./dist/icons/achievements` & `./src/Achievements/AchievementData.json`

@ -1,429 +1,18 @@
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import { SkillNames } from "./Bladeburner/data/SkillNames";
import { Skills } from "./Bladeburner/Skills";
import { CONSTANTS } from "./Constants";
import { Industries } from "./Corporation/IndustryData";
import { Exploit } from "./Exploits/Exploit";
import { Factions } from "./Faction/Factions";
import { AllGangs } from "./Gang/AllGangs";
import { GangConstants } from "./Gang/data/Constants";
import { HacknetNodeConstants, HacknetServerConstants } from "./Hacknet/data/Constants";
import { hasHacknetServers } from "./Hacknet/HacknetHelpers";
import { HacknetNode } from "./Hacknet/HacknetNode";
import { HacknetServer } from "./Hacknet/HacknetServer";
import { CityName } from "./Locations/data/CityNames";
import { Player } from "./Player";
import { Programs } from "./Programs/Programs";
import { isScriptFilename } from "./Script/isScriptFilename";
import { Script } from "./Script/Script";
import { GetAllServers, GetServer } from "./Server/AllServers";
import { SpecialServers } from "./Server/data/SpecialServers";
import { Server } from "./Server/Server";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
import { removeLeadingSlash } from "./Terminal/DirectoryHelpers";
import { Terminal } from "./Terminal";
import { SnackbarEvents } from "./ui/React/Snackbar";
import { IMap } from "./types";
interface Achievement {
ID: string;
Condition: () => boolean;
}
function bitNodeFinishedState(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) return false;
if (wd.backdoorInstalled) return true;
return Player.bladeburner !== null && Player.bladeburner.blackops.hasOwnProperty("Operation Daedalus");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function sfAchievement(): Achievement[] {
const achs: Achievement[] = [];
for (let i = 0; i <= 11; i++) {
for (let j = 1; j <= 3; j++) {
achs.push({
ID: `SF${i}.${j}`,
Condition: () => Player.sourceFileLvl(i) >= j,
});
}
}
return achs;
}
const achievements: Achievement[] = [
{ ID: "CYBERSEC", Condition: () => Player.factions.includes("CyberSec") },
{ ID: "NITESEC", Condition: () => Player.factions.includes("NiteSec") },
{ ID: "THE_BLACK_HAND", Condition: () => Player.factions.includes("The Black Hand") },
{ ID: "BITRUNNERS", Condition: () => Player.factions.includes("BitRunners") },
{ ID: "THE_COVENANT", Condition: () => Player.factions.includes("The Covenant") },
{ ID: "DAEDALUS", Condition: () => Player.factions.includes("Daedalus") },
{ ID: "ILLUMINATI", Condition: () => Player.factions.includes("Illuminati") },
{ ID: "BRUTESSH.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.BruteSSHProgram.name) },
{ ID: "FTPCRACK.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.FTPCrackProgram.name) },
{ ID: "RELAYSMTP.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.RelaySMTPProgram.name) },
{ ID: "HTTPWORM.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.HTTPWormProgram.name) },
{ ID: "SQLINJECT.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.SQLInjectProgram.name) },
{ ID: "FORMULAS.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.Formulas.name) },
{ ID: "SF1.1", Condition: () => Player.sourceFileLvl(1) >= 1 },
{ ID: "SF2.1", Condition: () => Player.sourceFileLvl(2) >= 1 },
{ ID: "SF3.1", Condition: () => Player.sourceFileLvl(3) >= 1 },
{ ID: "SF4.1", Condition: () => Player.sourceFileLvl(4) >= 1 },
{ ID: "SF5.1", Condition: () => Player.sourceFileLvl(5) >= 1 },
{ ID: "SF6.1", Condition: () => Player.sourceFileLvl(6) >= 1 },
{ ID: "SF7.1", Condition: () => Player.sourceFileLvl(7) >= 1 },
{ ID: "SF8.1", Condition: () => Player.sourceFileLvl(8) >= 1 },
{ ID: "SF9.1", Condition: () => Player.sourceFileLvl(9) >= 1 },
{ ID: "SF10.1", Condition: () => Player.sourceFileLvl(10) >= 1 },
{ ID: "SF11.1", Condition: () => Player.sourceFileLvl(11) >= 1 },
{ ID: "SF12.1", Condition: () => Player.sourceFileLvl(12) >= 1 },
{
ID: "MONEY_1Q",
Condition: () => Player.money >= 1e18,
},
{
ID: "MONEY_M1B",
Condition: () => Player.money <= -1e9,
},
{
ID: "INSTALL_1",
Condition: () => Player.augmentations.length >= 1,
},
{
ID: "INSTALL_100",
Condition: () => Player.augmentations.length >= 100,
},
{
ID: "QUEUE_40",
Condition: () => Player.queuedAugmentations.length >= 40,
},
{
ID: "HACKING_100000",
Condition: () => Player.hacking >= 100000,
},
{
ID: "COMBAT_3000",
Condition: () =>
Player.strength >= 3000 && Player.defense >= 3000 && Player.dexterity >= 3000 && Player.agility >= 3000,
},
{
ID: "NEUROFLUX_255",
Condition: () => Player.augmentations.some((a) => a.name === AugmentationNames.NeuroFluxGovernor && a.level >= 255),
},
{
ID: "NS2",
Condition: () =>
Player.getHomeComputer().scripts.some((s) => s.filename.endsWith(".js") || s.filename.endsWith(".ns")),
},
{ ID: "FROZE", Condition: () => location.href.includes("noScripts") },
{
ID: "RUNNING_SCRIPTS_1000",
Condition: () => {
let running = 0;
for (const s of GetAllServers()) {
running += s.runningScripts.length;
}
return running >= 1000;
},
},
{
ID: "DRAIN_SERVER",
Condition: () => {
for (const s of GetAllServers()) {
if (s instanceof Server) {
if (s.moneyMax > 0 && s.moneyAvailable === 0) return true;
}
}
return false;
},
},
{ ID: "MAX_RAM", Condition: () => Player.getHomeComputer().maxRam === CONSTANTS.HomeComputerMaxRam },
{ ID: "MAX_CORES", Condition: () => Player.getHomeComputer().cpuCores === 8 },
{ ID: "SCRIPTS_30", Condition: () => Player.getHomeComputer().scripts.length >= 30 },
{ ID: "KARMA_1000000", Condition: () => Player.karma <= -1e6 },
{ ID: "STOCK_1q", Condition: () => Player.moneySourceB.stock >= 1e15 },
{
ID: "DISCOUNT",
Condition: () => {
const p = GetServer("powerhouse-fitness");
if (!(p instanceof Server)) return false;
return p.backdoorInstalled;
},
},
{ ID: "SCRIPT_32GB", Condition: () => Player.getHomeComputer().scripts.some((s) => s.ramUsage >= 32) },
{ ID: "FIRST_HACKNET_NODE", Condition: () => !hasHacknetServers(Player) && Player.hacknetNodes.length > 0 },
{
ID: "30_HACKNET_NODE",
Condition: () => !hasHacknetServers(Player) && Player.hacknetNodes.length >= 30,
},
{
ID: "MAX_HACKNET_NODE",
Condition: () => {
if (hasHacknetServers(Player)) return false;
for (const h of Player.hacknetNodes) {
if (!(h instanceof HacknetNode)) return false;
if (
h.ram === HacknetNodeConstants.MaxRam &&
h.cores === HacknetNodeConstants.MaxCores &&
h.level === HacknetNodeConstants.MaxLevel
)
return true;
}
return false;
},
},
{ ID: "HACKNET_NODE_10M", Condition: () => !hasHacknetServers(Player) && Player.moneySourceB.hacknet >= 10e6 },
{ ID: "REPUTATION_10M", Condition: () => Object.values(Factions).some((f) => f.playerReputation >= 10e6) },
{ ID: "DONATION", Condition: () => Object.values(Factions).some((f) => f.favor >= 150) },
{ ID: "TRAVEL", Condition: () => Player.city !== CityName.Sector12 },
{
ID: "WORKOUT",
Condition: () =>
[
CONSTANTS.ClassGymStrength,
CONSTANTS.ClassGymDefense,
CONSTANTS.ClassGymDexterity,
CONSTANTS.ClassGymAgility,
].includes(Player.className),
},
{ ID: "TOR", Condition: () => Player.hasTorRouter() },
{ ID: "HOSPITALIZED", Condition: () => Player.moneySourceB.hospitalization !== 0 },
{ ID: "GANG", Condition: () => Player.gang !== null },
{
ID: "FULL_GANG",
Condition: () => Player.gang !== null && Player.gang.members.length === GangConstants.MaximumGangMembers,
},
{
ID: "GANG_TERRITORY",
Condition: () => Player.gang !== null && AllGangs[Player.gang.facName].territory >= 0.999,
},
{
ID: "GANG_MEMBER_POWER",
Condition: () =>
Player.gang !== null &&
Player.gang.members.some(
(m) =>
m.hack >= 10000 || m.str >= 10000 || m.def >= 10000 || m.dex >= 10000 || m.agi >= 10000 || m.cha >= 10000,
),
},
{ ID: "CORPORATION", Condition: () => Player.corporation !== null },
{
ID: "CORPORATION_BRIBE",
Condition: () => Player.corporation !== null && Player.corporation.unlockUpgrades[6] === 1,
},
{
ID: "CORPORATION_PROD_1000",
Condition: () => Player.corporation !== null && Player.corporation.divisions.some((d) => d.prodMult >= 1000),
},
{
ID: "CORPORATION_EMPLOYEE_3000",
Condition: () => {
if (Player.corporation === null) return false;
for (const d of Player.corporation.divisions) {
for (const o of Object.values(d.offices)) {
if (o === 0) continue;
if (o.employees.length > 3000) return true;
}
}
return false;
},
},
{
ID: "CORPORATION_REAL_ESTATE",
Condition: () =>
Player.corporation !== null && Player.corporation.divisions.some((d) => d.type === Industries.RealEstate),
},
{ ID: "INTELLIGENCE_255", Condition: () => Player.intelligence >= 255 },
{ ID: "BLADEBURNER_DIVISION", Condition: () => Player.bladeburner !== null },
{
ID: "BLADEBURNER_OVERCLOCK",
Condition: () =>
Player.bladeburner !== null &&
Player.bladeburner.skills[SkillNames.Overclock] === Skills[SkillNames.Overclock].maxLvl,
},
{
ID: "BLADEBURNER_UNSPENT_100000",
Condition: () => Player.bladeburner !== null && Player.bladeburner.skillPoints >= 100000,
},
{ ID: "4S", Condition: () => Player.has4SData },
{ ID: "FIRST_HACKNET_SERVER", Condition: () => hasHacknetServers(Player) && Player.hacknetNodes.length > 0 },
{
ID: "ALL_HACKNET_SERVER",
Condition: () => hasHacknetServers(Player) && Player.hacknetNodes.length === HacknetServerConstants.MaxServers,
},
{
ID: "MAX_HACKNET_SERVER",
Condition: () => {
if (!hasHacknetServers(Player)) return false;
for (const h of Player.hacknetNodes) {
if (typeof h !== "string") return false;
const hs = GetServer(h);
if (!(hs instanceof HacknetServer)) return false;
if (
hs.maxRam === HacknetServerConstants.MaxRam &&
hs.cores === HacknetServerConstants.MaxCores &&
hs.level === HacknetServerConstants.MaxLevel &&
hs.cache === HacknetServerConstants.MaxCache
)
return true;
}
return false;
},
},
{ ID: "HACKNET_SERVER_1B", Condition: () => hasHacknetServers(Player) && Player.moneySourceB.hacknet >= 1e9 },
{
ID: "MAX_CACHE",
Condition: () => hasHacknetServers(Player) && Player.hashManager.hashes === Player.hashManager.capacity,
},
{
ID: "SLEEVE_8",
Condition: () => Player.sleeves.length === 8,
},
{
ID: "FAST_BN",
Condition: () => bitNodeFinishedState() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2,
},
{
ID: "INDECISIVE",
Condition: (function () {
let c = 0;
setInterval(() => {
if (Router.page() === Page.BitVerse) {
c++;
} else {
c = 0;
}
}, 60 * 1000);
return () => c > 60;
})(),
},
{
ID: "CHALLENGE_BN1",
Condition: () =>
Player.bitNodeN === 1 &&
bitNodeFinishedState() &&
Player.getHomeComputer().maxRam <= 128 &&
Player.getHomeComputer().cpuCores === 1,
},
{
ID: "CHALLENGE_BN2",
Condition: () => Player.bitNodeN === 2 && bitNodeFinishedState() && Player.gang === null,
},
{
ID: "CHALLENGE_BN3",
Condition: () => Player.bitNodeN === 3 && bitNodeFinishedState() && Player.corporation === null,
},
{
ID: "CHALLENGE_BN6",
Condition: () => Player.bitNodeN === 6 && bitNodeFinishedState() && Player.bladeburner === null,
},
{
ID: "CHALLENGE_BN7",
Condition: () => Player.bitNodeN === 7 && bitNodeFinishedState() && Player.bladeburner === null,
},
{
ID: "CHALLENGE_BN8",
Condition: () => Player.bitNodeN === 8 && bitNodeFinishedState() && !Player.has4SData && !Player.has4SDataTixApi,
},
{
ID: "CHALLENGE_BN9",
Condition: () =>
Player.bitNodeN === 9 &&
bitNodeFinishedState() &&
Player.moneySourceB.hacknet === 0 &&
Player.moneySourceB.hacknet_expenses === 0,
},
{
ID: "CHALLENGE_BN10",
Condition: () =>
Player.bitNodeN === 10 &&
bitNodeFinishedState() &&
!Player.sleeves.some(
(s) =>
s.augmentations.length > 0 ||
s.hacking_exp > 0 ||
s.strength_exp > 0 ||
s.defense_exp > 0 ||
s.agility_exp > 0 ||
s.dexterity_exp > 0 ||
s.charisma_exp > 0,
),
},
{ ID: "CHALLENGE_BN12", Condition: () => Player.sourceFileLvl(12) >= 50 },
{
ID: "CHALLENGE_BN13",
Condition: () =>
Player.bitNodeN === 13 &&
bitNodeFinishedState() &&
!Player.augmentations.some((a) => a.name === AugmentationNames.StaneksGift1),
},
{ ID: "BYPASS", Condition: () => Player.exploits.includes(Exploit.Bypass) },
{ ID: "PROTOTYPETAMPERING", Condition: () => Player.exploits.includes(Exploit.PrototypeTampering) },
{ ID: "UNCLICKABLE", Condition: () => Player.exploits.includes(Exploit.Unclickable) },
{ ID: "UNDOCUMENTEDFUNCTIONCALL", Condition: () => Player.exploits.includes(Exploit.UndocumentedFunctionCall) },
{ ID: "TIMECOMPRESSION", Condition: () => Player.exploits.includes(Exploit.TimeCompression) },
{ ID: "REALITYALTERATION", Condition: () => Player.exploits.includes(Exploit.RealityAlteration) },
{ ID: "N00DLES", Condition: () => Player.exploits.includes(Exploit.N00dles) },
{ ID: "EDITSAVEFILE", Condition: () => Player.exploits.includes(Exploit.EditSaveFile) },
{ ID: "DEVMENU", Condition: () => Player.exploits.includes(Exploit.YoureNotMeantToAccessThis) },
{
ID: "UNACHIEVABLE",
// Hey Players! Yes, you're supposed to modify this to get the achievement!
Condition: () => false,
},
// Steam has a limit of 100 achievement. So these were planned but commented for now.
// { ID: "ECORP", Condition: () => Player.factions.includes("ECorp") },
// { ID: "MEGACORP", Condition: () => Player.factions.includes("MegaCorp") },
// { ID: "BACHMAN_&_ASSOCIATES", Condition: () => Player.factions.includes("Bachman & Associates") },
// { ID: "BLADE_INDUSTRIES", Condition: () => Player.factions.includes("Blade Industries") },
// { ID: "NWO", Condition: () => Player.factions.includes("NWO") },
// { ID: "CLARKE_INCORPORATED", Condition: () => Player.factions.includes("Clarke Incorporated") },
// { ID: "OMNITEK_INCORPORATED", Condition: () => Player.factions.includes("OmniTek Incorporated") },
// { ID: "FOUR_SIGMA", Condition: () => Player.factions.includes("Four Sigma") },
// { ID: "KUAIGONG_INTERNATIONAL", Condition: () => Player.factions.includes("KuaiGong International") },
// { ID: "FULCRUM_SECRET_TECHNOLOGIES", Condition: () => Player.factions.includes("Fulcrum Secret Technologies") },
// { ID: "AEVUM", Condition: () => Player.factions.includes("Aevum") },
// { ID: "CHONGQING", Condition: () => Player.factions.includes("Chongqing") },
// { ID: "ISHIMA", Condition: () => Player.factions.includes("Ishima") },
// { ID: "NEW_TOKYO", Condition: () => Player.factions.includes("New Tokyo") },
// { ID: "SECTOR-12", Condition: () => Player.factions.includes("Sector-12") },
// { ID: "VOLHAVEN", Condition: () => Player.factions.includes("Volhaven") },
// { ID: "SPEAKERS_FOR_THE_DEAD", Condition: () => Player.factions.includes("Speakers for the Dead") },
// { ID: "THE_DARK_ARMY", Condition: () => Player.factions.includes("The Dark Army") },
// { ID: "THE_SYNDICATE", Condition: () => Player.factions.includes("The Syndicate") },
// { ID: "SILHOUETTE", Condition: () => Player.factions.includes("Silhouette") },
// { ID: "TETRADS", Condition: () => Player.factions.includes("Tetrads") },
// { ID: "SLUM_SNAKES", Condition: () => Player.factions.includes("Slum Snakes") },
// { ID: "NETBURNERS", Condition: () => Player.factions.includes("Netburners") },
// { ID: "TIAN_DI_HUI", Condition: () => Player.factions.includes("Tian Di Hui") },
// { ID: "BLADEBURNERS", Condition: () => Player.factions.includes("Bladeburners") },
// { ID: "DEEPSCANV1.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.DeepscanV1.name) },
// { ID: "DEEPSCANV2.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.DeepscanV2.name) },
// {
// ID: "SERVERPROFILER.EXE",
// Condition: () => Player.getHomeComputer().programs.includes(Programs.ServerProfiler.name),
// },
// { ID: "AUTOLINK.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.AutoLink.name) },
// { ID: "FLIGHT.EXE", Condition: () => Player.getHomeComputer().programs.includes(Programs.Flight.name) },
];
function setAchievements(achs: string[]): void {
(document as any).achievements = achs;
}
function calculateAchievements(): void {
setAchievements(achievements.filter((a) => a.Condition()).map((a) => a.ID));
}
import { GetServer } from "./Server/AllServers";
export function initElectron(): void {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf(" electron/") > -1) {
// Electron-specific code
setAchievements([]);
(document as any).achievements = [];
initWebserver();
setInterval(calculateAchievements, 5000);
initAppNotifier();
}
}

@ -19,6 +19,7 @@ export interface IEngine {
messages: number;
mechanicProcess: number;
contractGeneration: number;
achievementsCounter: number;
};
decrementAllCounters: (numCycles?: number) => void;
checkCounters: () => void;

@ -30,6 +30,7 @@ import { IRouter } from "../ui/Router";
import { WorkerScript } from "../Netscript/WorkerScript";
import { HacknetServer } from "../Hacknet/HacknetServer";
import { ISkillProgress } from "./formulas/skill";
import { PlayerAchievement } from "../Achievements/Achievements";
export interface IPlayer {
// Class members
@ -70,6 +71,7 @@ export interface IPlayer {
sleevesFromCovenant: number;
sourceFiles: IPlayerOwnedSourceFile[];
exploits: Exploit[];
achievements: PlayerAchievement[];
lastUpdate: number;
totalPlaytime: number;
@ -238,6 +240,7 @@ export interface IPlayer {
takeDamage(amt: number): boolean;
travel(to: CityName): boolean;
giveExploit(exploit: Exploit): void;
giveAchievement(achievementId: string): void;
queryStatFromString(str: string): number;
getIntelligenceBonus(weight: number): number;
getCasinoWinnings(): number;

@ -35,6 +35,7 @@ import { CityName } from "../../Locations/data/CityNames";
import { MoneySourceTracker } from "../../utils/MoneySourceTracker";
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../../utils/JSONReviver";
import { ISkillProgress } from "../formulas/skill";
import { PlayerAchievement } from '../../Achievements/Achievements';
export class PlayerObject implements IPlayer {
// Class members
@ -75,6 +76,7 @@ export class PlayerObject implements IPlayer {
sleevesFromCovenant: number;
sourceFiles: IPlayerOwnedSourceFile[];
exploits: Exploit[];
achievements: PlayerAchievement[];
lastUpdate: number;
totalPlaytime: number;
@ -243,6 +245,7 @@ export class PlayerObject implements IPlayer {
takeDamage: (amt: number) => boolean;
travel: (to: CityName) => boolean;
giveExploit: (exploit: Exploit) => void;
giveAchievement: (achievementId: string) => void;
queryStatFromString: (str: string) => number;
getIntelligenceBonus: (weight: number) => number;
getCasinoWinnings: () => number;
@ -467,6 +470,7 @@ export class PlayerObject implements IPlayer {
this.scriptProdSinceLastAug = 0;
this.exploits = [];
this.achievements = [];
this.init = generalMethods.init;
this.prestigeAugmentation = generalMethods.prestigeAugmentation;
@ -557,6 +561,7 @@ export class PlayerObject implements IPlayer {
this.gotoLocation = generalMethods.gotoLocation;
this.canAccessResleeving = generalMethods.canAccessResleeving;
this.giveExploit = generalMethods.giveExploit;
this.giveAchievement = generalMethods.giveAchievement;
this.getIntelligenceBonus = generalMethods.getIntelligenceBonus;
this.getCasinoWinnings = generalMethods.getCasinoWinnings;
this.hasAugmentation = augmentationMethods.hasAugmentation;

@ -64,6 +64,7 @@ import React from "react";
import { serverMetadata } from "../../Server/data/servers";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { calculateClassEarnings } from "../formulas/work";
import { achievements } from "../../Achievements/Achievements";
export function init(this: IPlayer): void {
/* Initialize Player's home computer */
@ -2632,6 +2633,15 @@ export function giveExploit(this: IPlayer, exploit: Exploit): void {
}
}
export function giveAchievement(this: IPlayer, achievementId: string): void {
const achievement = achievements[achievementId];
if (!achievement) return;
if (!this.achievements.map(a => a.ID).includes(achievementId)) {
this.achievements.push({ ID: achievementId, unlockedOn: new Date().getTime() });
SnackbarEvents.emit(`Unlocked Achievement: "${achievement.Name}"`, 'success', 2000);
}
}
export function getIntelligenceBonus(this: IPlayer, weight: number): number {
return calculateIntelligenceBonus(this.intelligence, weight);
}

@ -37,6 +37,7 @@ import CheckIcon from "@mui/icons-material/Check"; // Milestones
import HelpIcon from "@mui/icons-material/Help"; // Tutorial
import SettingsIcon from "@mui/icons-material/Settings"; // options
import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard"; // Dev
import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; // Achievements
import AccountBoxIcon from "@mui/icons-material/AccountBox";
import PublicIcon from "@mui/icons-material/Public";
import LiveHelpIcon from "@mui/icons-material/LiveHelp";
@ -256,6 +257,10 @@ export function SidebarRoot(props: IProps): React.ReactElement {
props.router.toDevMenu();
}
function clickAchievements(): void {
props.router.toAchievements();
}
useEffect(() => {
// Shortcuts to navigate through the game
// Alt-t - Terminal
@ -747,6 +752,21 @@ export function SidebarRoot(props: IProps): React.ReactElement {
</Typography>
</ListItemText>
</ListItem>
<ListItem
button
key={"Achievements"}
className={clsx({
[classes.active]: props.page === Page.Achievements,
})}
onClick={clickAchievements}
>
<ListItemIcon>
<EmojiEventsIcon color={props.page !== Page.Achievements ? "secondary" : "primary"} />
</ListItemIcon>
<ListItemText>
<Typography color={props.page !== Page.Achievements ? "secondary" : "primary"}>Achievements</Typography>
</ListItemText>
</ListItem>
<ListItem
button
key={"Options"}

315
src/ThirdParty/colorUtils.ts vendored Normal file

@ -0,0 +1,315 @@
// @ts-nocheck
/* tslint:disable */
/* eslint-disable */
/*
Taken from:
https://codepen.io/sosuke/pen/Pjoqqp
--------------------------------------------------------
This utility transform an hex color into css filter rules.
Useful to change color of a black image to match a given color
*/
class Color {
constructor(r, g, b) {
this.set(r, g, b);
}
toString() {
return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`;
}
set(r, g, b) {
this.r = this.clamp(r);
this.g = this.clamp(g);
this.b = this.clamp(b);
}
hueRotate(angle = 0) {
angle = angle / 180 * Math.PI;
const sin = Math.sin(angle);
const cos = Math.cos(angle);
this.multiply([
0.213 + cos * 0.787 - sin * 0.213,
0.715 - cos * 0.715 - sin * 0.715,
0.072 - cos * 0.072 + sin * 0.928,
0.213 - cos * 0.213 + sin * 0.143,
0.715 + cos * 0.285 + sin * 0.140,
0.072 - cos * 0.072 - sin * 0.283,
0.213 - cos * 0.213 - sin * 0.787,
0.715 - cos * 0.715 + sin * 0.715,
0.072 + cos * 0.928 + sin * 0.072,
]);
}
grayscale(value = 1) {
this.multiply([
0.2126 + 0.7874 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 + 0.2848 * (1 - value),
0.0722 - 0.0722 * (1 - value),
0.2126 - 0.2126 * (1 - value),
0.7152 - 0.7152 * (1 - value),
0.0722 + 0.9278 * (1 - value),
]);
}
sepia(value = 1) {
this.multiply([
0.393 + 0.607 * (1 - value),
0.769 - 0.769 * (1 - value),
0.189 - 0.189 * (1 - value),
0.349 - 0.349 * (1 - value),
0.686 + 0.314 * (1 - value),
0.168 - 0.168 * (1 - value),
0.272 - 0.272 * (1 - value),
0.534 - 0.534 * (1 - value),
0.131 + 0.869 * (1 - value),
]);
}
saturate(value = 1) {
this.multiply([
0.213 + 0.787 * value,
0.715 - 0.715 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 + 0.285 * value,
0.072 - 0.072 * value,
0.213 - 0.213 * value,
0.715 - 0.715 * value,
0.072 + 0.928 * value,
]);
}
multiply(matrix) {
const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
this.r = newR;
this.g = newG;
this.b = newB;
}
brightness(value = 1) {
this.linear(value);
}
contrast(value = 1) {
this.linear(value, -(0.5 * value) + 0.5);
}
linear(slope = 1, intercept = 0) {
this.r = this.clamp(this.r * slope + intercept * 255);
this.g = this.clamp(this.g * slope + intercept * 255);
this.b = this.clamp(this.b * slope + intercept * 255);
}
invert(value = 1) {
this.r = this.clamp((value + this.r / 255 * (1 - 2 * value)) * 255);
this.g = this.clamp((value + this.g / 255 * (1 - 2 * value)) * 255);
this.b = this.clamp((value + this.b / 255 * (1 - 2 * value)) * 255);
}
hsl() {
// Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
const r = this.r / 255;
const g = this.g / 255;
const b = this.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return {
h: h * 100,
s: s * 100,
l: l * 100,
};
}
clamp(value) {
if (value > 255) {
value = 255;
} else if (value < 0) {
value = 0;
}
return value;
}
}
export class Solver {
constructor(target) {
this.target = target;
this.targetHSL = target.hsl();
this.reusedColor = new Color(0, 0, 0);
}
solve() {
const result = this.solveNarrow(this.solveWide());
return {
values: result.values,
loss: result.loss,
filter: this.css(result.values),
};
}
solveWide() {
const A = 5;
const c = 15;
const a = [60, 180, 18000, 600, 1.2, 1.2];
let best = { loss: Infinity };
for (let i = 0; best.loss > 25 && i < 3; i++) {
const initial = [50, 20, 3750, 50, 100, 100];
const result = this.spsa(A, a, c, initial, 1000);
if (result.loss < best.loss) {
best = result;
}
}
return best;
}
solveNarrow(wide) {
const A = wide.loss;
const c = 2;
const A1 = A + 1;
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
return this.spsa(A, a, c, wide.values, 500);
}
spsa(A, a, c, values, iters) {
const alpha = 1;
const gamma = 0.16666666666666666;
let best = null;
let bestLoss = Infinity;
const deltas = new Array(6);
const highArgs = new Array(6);
const lowArgs = new Array(6);
for (let k = 0; k < iters; k++) {
const ck = c / Math.pow(k + 1, gamma);
for (let i = 0; i < 6; i++) {
deltas[i] = Math.random() > 0.5 ? 1 : -1;
highArgs[i] = values[i] + ck * deltas[i];
lowArgs[i] = values[i] - ck * deltas[i];
}
const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
for (let i = 0; i < 6; i++) {
const g = lossDiff / (2 * ck) * deltas[i];
const ak = a[i] / Math.pow(A + k + 1, alpha);
values[i] = fix(values[i] - ak * g, i);
}
const loss = this.loss(values);
if (loss < bestLoss) {
best = values.slice(0);
bestLoss = loss;
}
}
return { values: best, loss: bestLoss };
function fix(value, idx) {
let max = 100;
if (idx === 2 /* saturate */) {
max = 7500;
} else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
max = 200;
}
if (idx === 3 /* hue-rotate */) {
if (value > max) {
value %= max;
} else if (value < 0) {
value = max + value % max;
}
} else if (value < 0) {
value = 0;
} else if (value > max) {
value = max;
}
return value;
}
}
loss(filters) {
// Argument is array of percentages.
const color = this.reusedColor;
color.set(0, 0, 0);
color.invert(filters[0] / 100);
color.sepia(filters[1] / 100);
color.saturate(filters[2] / 100);
color.hueRotate(filters[3] * 3.6);
color.brightness(filters[4] / 100);
color.contrast(filters[5] / 100);
const colorHSL = color.hsl();
return (
Math.abs(color.r - this.target.r) +
Math.abs(color.g - this.target.g) +
Math.abs(color.b - this.target.b) +
Math.abs(colorHSL.h - this.targetHSL.h) +
Math.abs(colorHSL.s - this.targetHSL.s) +
Math.abs(colorHSL.l - this.targetHSL.l)
);
}
css(filters) {
function fmt(idx, multiplier = 1) {
return Math.round(filters[idx] * multiplier);
}
return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%)`
}
}
function hexToRgb(hex): number[] {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
]
: null;
}
export function getFiltersFromHex(hex): string {
const rgb = hexToRgb(hex);
if (!rgb) return ''
const [r, g, b] = rgb;
const color = new Color(r, g, b);
const solver = new Solver(color);
return solver.solve().filter;
}

@ -44,6 +44,7 @@ import { AlertEvents } from "./ui/React/AlertManager";
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
import { startExploits } from "./Exploits/loops";
import { calculateAchievements } from "./Achievements/Achievements";
import React from "react";
import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
@ -65,6 +66,7 @@ const Engine: {
messages: number;
mechanicProcess: number;
contractGeneration: number;
achievementsCounter: number;
};
decrementAllCounters: (numCycles?: number) => void;
checkCounters: () => void;
@ -163,6 +165,7 @@ const Engine: {
messages: 150,
mechanicProcess: 5, // Processes certain mechanics (Corporation, Bladeburner)
contractGeneration: 3000, // Generate Coding Contracts
achievementsCounter: 300, // Check if we have new achievements
},
decrementAllCounters: function (numCycles = 1) {
@ -234,6 +237,11 @@ const Engine: {
}
Engine.Counters.contractGeneration = 3000;
}
if (Engine.Counters.achievementsCounter <= 0) {
calculateAchievements();
Engine.Counters.achievementsCounter = 300;
}
},
load: function (saveString) {

@ -76,6 +76,7 @@ import { InvitationModal } from "../Faction/ui/InvitationModal";
import { enterBitNode } from "../RedPill";
import { Context } from "./Context";
import { RecoveryMode, RecoveryRoot } from "./React/RecoveryRoot";
import { AchievementsRoot } from "../Achievements/AchievementsRoot";
const htmlLocation = location;
@ -183,6 +184,9 @@ export let Router: IRouter = {
toStaneksGift: () => {
throw new Error("Router called before initialization");
},
toAchievements: () => {
throw new Error("Router called before initialization");
}
};
function determineStartPage(player: IPlayer): Page {
@ -287,6 +291,9 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
toStaneksGift: () => {
setPage(Page.StaneksGift);
},
toAchievements: () => {
setPage(Page.Achievements);
},
};
useEffect(() => {
@ -408,6 +415,8 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
Router.toTerminal();
}}
/>
) : page === Page.Achievements ? (
<AchievementsRoot />
) : (
<>
<Typography>Cannot load</Typography>

@ -36,6 +36,7 @@ export enum Page {
Loading,
StaneksGift,
Recovery,
Achievements,
}
export interface ScriptEditorRouteOptions {
@ -80,4 +81,5 @@ export interface IRouter {
toBladeburnerCinematic(): void;
toLocation(location: Location): void;
toStaneksGift(): void;
toAchievements(): void;
}

@ -10,3 +10,13 @@ It decodes the save and prettifies the output. Canno be used to modify a save ga
```sh
node ./pretty-save.js 'C:\\Users\\martin\\Desktop\\bitburnerSave_1641395736_BN12x14.json' 'C:\\Users\\martin\\Desktop\\pretty.json'
```
## Fetch Steam Achievements Data
Used to synchronize the achievements info in steamworks to the game's data.json
**Usage**
```sh
# Get your key here: https://steamcommunity.com/dev/apikey
node fetch-steam-achievements-data.js DEVKEYDEVKEYDEVKEYDEVKEY
```

@ -0,0 +1,69 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const https = require('https')
const fs = require('fs').promises;
const path = require('path');
const key = process.argv[2]
function getRawJSON() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.steampowered.com',
port: 443,
path: `/ISteamUserStats/GetSchemaForGame/v0002/?appid=1812820&key=${key}`,
method: 'GET',
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
}
}
let data = [];
const req = https.request(options, res => {
console.log(`statusCode: ${res.statusCode}`)
res.on('data', chunk => {
data.push(chunk)
})
res.on('end', () => {
console.log('Response ended: ');
resolve(Buffer.concat(data).toString());
});
})
req.on('error', error => {
console.error(error)
req.end();
reject(error);
});
req.end();
});
}
async function fetchAchievementsData() {
const raw = await getRawJSON();
const o = JSON.parse(raw);
const achievements = {};
o.game.availableGameStats.achievements.forEach((a) => {
achievements[a.name] = {
ID: a.name,
Name: a.displayName,
Description: a.description,
};
})
const data = {
note: '***** Generated from a script, overwritten by steam achievements data *****',
fetchedOn: new Date().getTime(),
achievements,
}
const jsonPath = path.resolve(__dirname, '../src/Achievements/AchievementData.json');
await fs.writeFile(jsonPath, JSON.stringify(data, null, 2));
return data;
}
fetchAchievementsData().
then((json) => console.log(JSON.stringify(json, null, 2)));

@ -9,7 +9,8 @@
"target": "es6",
"sourceMap": true,
"strict": true,
"types": ["cypress", "@testing-library/cypress", "node", "raw-loader.d.ts"]
"resolveJsonModule": true,
"types": ["cypress", "@testing-library/cypress", "node"]
},
"exclude": ["node_modules"]
}

@ -142,7 +142,7 @@ module.exports = (env, argv) => {
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
test: /\.(js$|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",