diff --git a/assets/Steam/achievements/pack-for-web.sh b/assets/Steam/achievements/pack-for-web.sh
new file mode 100644
index 000000000..f00431ff2
--- /dev/null
+++ b/assets/Steam/achievements/pack-for-web.sh
@@ -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
diff --git a/package.sh b/package.sh
index 94dbb9306..866be1700 100755
--- a/package.sh
+++ b/package.sh
@@ -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
diff --git a/src/Achievements/AchievementData.json b/src/Achievements/AchievementData.json
new file mode 100644
index 000000000..7228ea889
--- /dev/null
+++ b/src/Achievements/AchievementData.json
@@ -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."
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Achievements/AchievementEntry.tsx b/src/Achievements/AchievementEntry.tsx
new file mode 100644
index 000000000..0d3131a7a
--- /dev/null
+++ b/src/Achievements/AchievementEntry.tsx
@@ -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 (
+
+
+
+
+
+ {achievement.Name}
+
+
+ {achievement.Description}
+
+ {isUnlocked && (
+
+ Acquired on {achievedOn}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/Achievements/AchievementIcon.tsx b/src/Achievements/AchievementIcon.tsx
new file mode 100644
index 000000000..7bf576b9b
--- /dev/null
+++ b/src/Achievements/AchievementIcon.tsx
@@ -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 (
+
+
setImgLoaded(true)}
+ alt={achievement.Name} />
+
+ );
+}
diff --git a/src/Achievements/AchievementList.tsx b/src/Achievements/AchievementList.tsx
new file mode 100644
index 000000000..b590cf38c
--- /dev/null
+++ b/src/Achievements/AchievementList.tsx
@@ -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 (
+
+
+ {unlocked.length > 0 && (
+
+
+
+ Acquired ({unlocked.length}/{data.length})
+
+
+
+ {unlocked.map(item => (
+
+ ))}
+
+
+ )}
+
+ {locked.length > 0 && (
+
+
+
+ Locked ({locked.length} remaining)
+
+
+
+ {locked.map(item => (
+
+ ))}
+
+
+ )}
+
+ {unavailable.length > 0 && (
+
+
+
+ Unavailable ({unavailable.length} remaining)
+
+
+
+
+ {unavailable.length} additional achievements hidden behind content you don't have access to.
+
+
+
+ )}
+
+ {secret.length > 0 && (
+
+
+
+ Secret ({secret.length} remaining)
+
+
+
+
+ {secret.map(item => (
+ <>
+
+
+ >
+ ))}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts
new file mode 100644
index 000000000..368c6bb15
--- /dev/null
+++ b/src/Achievements/Achievements.ts
@@ -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 = (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;
+}
+
+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 = {
+ "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)];
+}
diff --git a/src/Achievements/AchievementsRoot.tsx b/src/Achievements/AchievementsRoot.tsx
new file mode 100644
index 000000000..12bf0cd93
--- /dev/null
+++ b/src/Achievements/AchievementsRoot.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/Achievements/README.md b/src/Achievements/README.md
new file mode 100644
index 000000000..414145b13
--- /dev/null
+++ b/src/Achievements/README.md
@@ -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`
diff --git a/src/Electron.tsx b/src/Electron.tsx
index 4f7221e4f..b4f4ba684 100644
--- a/src/Electron.tsx
+++ b/src/Electron.tsx
@@ -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();
}
}
diff --git a/src/IEngine.ts b/src/IEngine.ts
index d495e5e20..5f99fdf97 100644
--- a/src/IEngine.ts
+++ b/src/IEngine.ts
@@ -19,6 +19,7 @@ export interface IEngine {
messages: number;
mechanicProcess: number;
contractGeneration: number;
+ achievementsCounter: number;
};
decrementAllCounters: (numCycles?: number) => void;
checkCounters: () => void;
diff --git a/src/PersonObjects/IPlayer.ts b/src/PersonObjects/IPlayer.ts
index 3153cb2b5..e6aa717c5 100644
--- a/src/PersonObjects/IPlayer.ts
+++ b/src/PersonObjects/IPlayer.ts
@@ -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;
diff --git a/src/PersonObjects/Player/PlayerObject.ts b/src/PersonObjects/Player/PlayerObject.ts
index 8497e94a1..07d5bacef 100644
--- a/src/PersonObjects/Player/PlayerObject.ts
+++ b/src/PersonObjects/Player/PlayerObject.ts
@@ -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;
diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx
index 23ca8ff3e..d4cd65e2e 100644
--- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx
+++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx
@@ -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);
}
diff --git a/src/Sidebar/ui/SidebarRoot.tsx b/src/Sidebar/ui/SidebarRoot.tsx
index 132758e92..0e51a2175 100644
--- a/src/Sidebar/ui/SidebarRoot.tsx
+++ b/src/Sidebar/ui/SidebarRoot.tsx
@@ -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 {
+
+
+
+
+
+ Achievements
+
+
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;
+}
diff --git a/src/engine.tsx b/src/engine.tsx
index d3afec3b7..4e2c34fa7 100644
--- a/src/engine.tsx
+++ b/src/engine.tsx
@@ -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) {
diff --git a/src/ui/GameRoot.tsx b/src/ui/GameRoot.tsx
index 4c37690a7..f9966ff0d 100644
--- a/src/ui/GameRoot.tsx
+++ b/src/ui/GameRoot.tsx
@@ -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,7 +415,9 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
Router.toTerminal();
}}
/>
- ) : (
+ ) : page === Page.Achievements ? (
+
+ ) : (
<>
Cannot load
>
diff --git a/src/ui/Router.ts b/src/ui/Router.ts
index 6b09983bb..4056a44ac 100644
--- a/src/ui/Router.ts
+++ b/src/ui/Router.ts
@@ -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;
}
diff --git a/tools/README.md b/tools/README.md
index f29ebd87e..0f2e5f759 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -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
+```
diff --git a/tools/fetch-steam-achievements-data.js b/tools/fetch-steam-achievements-data.js
new file mode 100644
index 000000000..aef79c8c3
--- /dev/null
+++ b/tools/fetch-steam-achievements-data.js
@@ -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)));
diff --git a/tsconfig.json b/tsconfig.json
index fcf8643fb..3cfda7488 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"]
}
diff --git a/webpack.config.js b/webpack.config.js
index f9203a87f..2b72bb10e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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",