diff --git a/assets/Steam/achievements/icons/BN14+.svg b/assets/Steam/achievements/icons/BN14+.svg
new file mode 100644
index 000000000..2036eaed9
--- /dev/null
+++ b/assets/Steam/achievements/icons/BN14+.svg
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/assets/Steam/achievements/icons/SF14.1.svg b/assets/Steam/achievements/icons/SF14.1.svg
new file mode 100644
index 000000000..31ac91b6b
--- /dev/null
+++ b/assets/Steam/achievements/icons/SF14.1.svg
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/assets/Steam/achievements/icons/ipvgo-anticheat.svg b/assets/Steam/achievements/icons/ipvgo-anticheat.svg
new file mode 100644
index 000000000..77490021b
--- /dev/null
+++ b/assets/Steam/achievements/icons/ipvgo-anticheat.svg
@@ -0,0 +1,74 @@
+
+
+
+
diff --git a/assets/Steam/achievements/icons/ipvgo-winning-streak.svg b/assets/Steam/achievements/icons/ipvgo-winning-streak.svg
new file mode 100644
index 000000000..24993670f
--- /dev/null
+++ b/assets/Steam/achievements/icons/ipvgo-winning-streak.svg
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/assets/Steam/achievements/template.svg b/assets/Steam/achievements/template.svg
index 7a99878c8..dc0d8f69c 100644
--- a/assets/Steam/achievements/template.svg
+++ b/assets/Steam/achievements/template.svg
@@ -56,13 +56,13 @@
id="layer1">
TEXT
diff --git a/dist/icons/achievements/BN14+.svg b/dist/icons/achievements/BN14+.svg
new file mode 100644
index 000000000..967d5febe
--- /dev/null
+++ b/dist/icons/achievements/BN14+.svg
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/dist/icons/achievements/SF14.1.svg b/dist/icons/achievements/SF14.1.svg
new file mode 100644
index 000000000..91df118eb
--- /dev/null
+++ b/dist/icons/achievements/SF14.1.svg
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/dist/icons/achievements/ipvgo-anticheat.svg b/dist/icons/achievements/ipvgo-anticheat.svg
new file mode 100644
index 000000000..28363990d
--- /dev/null
+++ b/dist/icons/achievements/ipvgo-anticheat.svg
@@ -0,0 +1,74 @@
+
+
+
+
diff --git a/dist/icons/achievements/ipvgo-winning-streak.svg b/dist/icons/achievements/ipvgo-winning-streak.svg
new file mode 100644
index 000000000..922b88559
--- /dev/null
+++ b/dist/icons/achievements/ipvgo-winning-streak.svg
@@ -0,0 +1,69 @@
+
+
+
+
diff --git a/src/Achievements/AchievementData.json b/src/Achievements/AchievementData.json
index 252262a54..a30a3e7ad 100644
--- a/src/Achievements/AchievementData.json
+++ b/src/Achievements/AchievementData.json
@@ -131,6 +131,11 @@
"Name": "They're lunatics",
"Description": "Acquire SF13.1"
},
+ "SF14.1": {
+ "ID": "SF14.1",
+ "Name": "IPvGO Subnet Takeover",
+ "Description": "Acquire SF14.1"
+ },
"MONEY_1Q": {
"ID": "MONEY_1Q",
"Name": "Here comes the money!",
@@ -391,6 +396,16 @@
"Name": "More BitNodes, please!",
"Description": "Reach SF x.3 in each BitNode."
},
+ "IPVGO_ANTICHEAT": {
+ "ID": "IPVGO_ANTICHEAT",
+ "Name": "IPvGO anticheat",
+ "Description": "Fail to cheat an IPvGO game and be ejected from the subnet."
+ },
+ "IPVGO_WINNING_STREAK": {
+ "ID": "IPVGO_WINNING_STREAK",
+ "Name": "Ten Steps Ahead",
+ "Description": "Get a winning streak of 10 against Illuminati."
+ },
"CHALLENGE_BN1": {
"ID": "CHALLENGE_BN1",
"Name": "BN1: Challenge",
@@ -436,6 +451,16 @@
"Name": "BN12: Challenge",
"Description": "Destroy BN12 50 times."
},
+ "CHALLENGE_BN13": {
+ "ID": "CHALLENGE_BN13",
+ "Name": "BN13: Challenge",
+ "Description": "Complete BN13 without Stanek's Gift."
+ },
+ "CHALLENGE_BN14": {
+ "ID": "CHALLENGE_BN14",
+ "Name": "BN14: Challenge",
+ "Description": "Destroy BN14 without making a move or cheating via IPvGO APIs."
+ },
"BYPASS": {
"ID": "BYPASS",
"Name": "Exploit: bypass",
@@ -481,11 +506,6 @@
"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: you're not meant to access this",
diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts
index 0c169afa4..77dce643c 100644
--- a/src/Achievements/Achievements.ts
+++ b/src/Achievements/Achievements.ts
@@ -29,16 +29,51 @@ import { workerScripts } from "../Netscript/WorkerScripts";
import { getRecordValues } from "../Types/Record";
import { ServerConstants } from "../Server/data/Constants";
-import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse, validBitNodes } from "../BitNode/BitNodeUtils";
+import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse } from "../BitNode/BitNodeUtils";
+import { validBitNodes } from "../BitNode/Constants";
import { isLegacyScript } from "../Paths/ScriptFilePath";
import { Settings } from "../Settings/Settings";
import { activateSteamAchievements } from "../Electron";
+import { Go } from "../Go/Go";
+import { type AchievementId, type SFAchievementId, SFAchievementIds } from "./Types";
-// Unable to correctly cast the JSON data into AchievementDataJson type otherwise...
-const achievementData = ((data)).achievements;
+function assertAchievements(
+ achievements: typeof data.achievements,
+): asserts achievements is AchievementDataJson["achievements"] {
+ for (const [key, value] of Object.entries(achievements)) {
+ if (key !== value.ID) {
+ throw new Error(`Invalid achievement ID. Key: ${key}. Value: ${value.ID}`);
+ }
+ }
+}
+
+/**
+ * The type of data.achievements is:
+ {
+ CYBERSEC: {
+ ID: string;
+ Name: string;
+ Description: string;
+ };
+ NITESEC: {
+ ID: string;
+ Name: string;
+ Description: string;
+ };
+ ...
+ }
+ * However, we want:
+ * - Typechecking at compile time: ID must be AchievementId, not string.
+ * - Runtime check: The value of ID must be the same as the key of the achievement. For example, with "CYBERSEC"
+ * achievement, the key is "CYBERSEC", so its ID must also be "CYBERSEC".
+ *
+ * We use assertAchievements to do the runtime check and assert the type.
+ */
+const achievementData = data.achievements;
+assertAchievements(achievementData);
export interface Achievement {
- ID: string;
+ ID: AchievementId;
Icon?: string;
Name?: string;
Description?: string;
@@ -46,48 +81,59 @@ export interface Achievement {
NotInSteam?: boolean;
Condition: () => boolean;
Visible?: () => boolean;
- AdditionalUnlock?: string[]; // IDs of achievements that should be awarded when awarding this one
+ AdditionalUnlock?: AchievementId[]; // IDs of achievements that should be awarded when awarding this one
}
export interface PlayerAchievement {
- ID: string;
+ ID: AchievementId;
unlockedOn?: number;
}
export interface AchievementDataJson {
- achievements: Record;
+ achievements: Record;
}
export interface AchievementData {
- ID: string;
+ ID: AchievementId;
Name: string;
Description: string;
}
-function sfAchievements(): Record {
- const achievements: Record = {};
- for (let i = 1; i <= 13; i++) {
- const ID = `SF${i}.1`;
- achievements[ID] = {
- ...achievementData[ID],
- Icon: ID,
+function sfAchievements(): Record {
+ const achievements = {} as Record;
+ for (const id of SFAchievementIds) {
+ const matchResult = id.match(/SF(\d{1,2})\.1/);
+ if (!matchResult) {
+ throw new Error(`Unexpected SFAchievementId: ${id}`);
+ }
+ const bn = Number.parseInt(matchResult[1]);
+ if (!validBitNodes.includes(bn)) {
+ throw new Error(`Unexpected BN value in SFAchievementId: ${id}`);
+ }
+ achievements[id] = {
+ /**
+ * The type of achievementData is still the original type (CYBERSEC: { ID: string; Name: string; Description: string; }).
+ * We have to typecast it here.
+ */
+ ...(achievementData as AchievementDataJson["achievements"])[id],
+ Icon: id,
Visible: knowAboutBitverse,
- Condition: () => Player.sourceFileLvl(i) >= 1,
- NotInSteam: i >= 13,
+ Condition: () => Player.sourceFileLvl(bn) >= 1,
+ NotInSteam: bn >= 13,
};
}
return achievements;
}
-export const achievements: Record = {
- [FactionName.CyberSec.toUpperCase()]: {
- ...achievementData[FactionName.CyberSec.toUpperCase()],
+export const achievements: Record = {
+ CYBERSEC: {
+ ...achievementData.CYBERSEC,
Icon: "CSEC",
Condition: () => Player.factions.includes(FactionName.CyberSec),
},
- [FactionName.NiteSec.toUpperCase()]: {
- ...achievementData[FactionName.NiteSec.toUpperCase()],
- Icon: FactionName.NiteSec,
+ NITESEC: {
+ ...achievementData.NITESEC,
+ Icon: "NiteSec",
Condition: () => Player.factions.includes(FactionName.NiteSec),
},
THE_BLACK_HAND: {
@@ -95,24 +141,24 @@ export const achievements: Record = {
Icon: "TBH",
Condition: () => Player.factions.includes(FactionName.TheBlackHand),
},
- [FactionName.BitRunners.toUpperCase()]: {
- ...achievementData[FactionName.BitRunners.toUpperCase()],
- Icon: FactionName.BitRunners.toLowerCase(),
+ BITRUNNERS: {
+ ...achievementData.BITRUNNERS,
+ Icon: "bitrunners",
Condition: () => Player.factions.includes(FactionName.BitRunners),
},
- [FactionName.Daedalus.toUpperCase()]: {
- ...achievementData[FactionName.Daedalus.toUpperCase()],
- Icon: FactionName.Daedalus.toLowerCase(),
+ DAEDALUS: {
+ ...achievementData.DAEDALUS,
+ Icon: "daedalus",
Condition: () => Player.factions.includes(FactionName.Daedalus),
},
THE_COVENANT: {
...achievementData.THE_COVENANT,
- Icon: FactionName.TheCovenant.toLowerCase().replace(/ /g, ""),
+ Icon: "thecovenant",
Condition: () => Player.factions.includes(FactionName.TheCovenant),
},
- [FactionName.Illuminati.toUpperCase()]: {
- ...achievementData[FactionName.Illuminati.toUpperCase()],
- Icon: FactionName.Illuminati.toLowerCase(),
+ ILLUMINATI: {
+ ...achievementData.ILLUMINATI,
+ Icon: "illuminati",
Condition: () => Player.factions.includes(FactionName.Illuminati),
},
"BRUTESSH.EXE": {
@@ -522,6 +568,20 @@ export const achievements: Record = {
Condition: () => validBitNodes.every((bn) => Player.sourceFileLvl(bn) >= 3),
NotInSteam: true,
},
+ IPVGO_ANTICHEAT: {
+ ...achievementData.IPVGO_ANTICHEAT,
+ Icon: "ipvgo-anticheat",
+ Visible: knowAboutBitverse,
+ Condition: () => false,
+ NotInSteam: true,
+ },
+ IPVGO_WINNING_STREAK: {
+ ...achievementData.IPVGO_WINNING_STREAK,
+ Icon: "ipvgo-winning-streak",
+ Visible: knowAboutBitverse,
+ Condition: () => false,
+ NotInSteam: true,
+ },
CHALLENGE_BN1: {
...achievementData.CHALLENGE_BN1,
Icon: "BN1+",
@@ -596,6 +656,22 @@ export const achievements: Record = {
Visible: () => canAccessBitNodeFeature(12),
Condition: () => Player.sourceFileLvl(12) >= 50,
},
+ CHALLENGE_BN13: {
+ ...achievementData.CHALLENGE_BN13,
+ Icon: "BN13+",
+ Visible: () => canAccessBitNodeFeature(13),
+ Condition: () =>
+ Player.bitNodeN === 13 &&
+ isBitNodeFinished() &&
+ !Player.augmentations.some((a) => a.name === AugmentationName.StaneksGift1),
+ },
+ CHALLENGE_BN14: {
+ ...achievementData.CHALLENGE_BN14,
+ Icon: "BN14+",
+ Visible: knowAboutBitverse,
+ Condition: () => Player.bitNodeN === 14 && isBitNodeFinished() && !Go.moveOrCheatViaApi,
+ NotInSteam: true,
+ },
BYPASS: {
...achievementData.BYPASS,
Icon: "SF-1",
@@ -651,15 +727,6 @@ export const achievements: Record = {
// Hey Players! Yes, you're supposed to modify this to get the achievement!
Condition: () => false,
},
- CHALLENGE_BN13: {
- ...achievementData.CHALLENGE_BN13,
- Icon: "BN13+",
- Visible: () => canAccessBitNodeFeature(13),
- Condition: () =>
- Player.bitNodeN === 13 &&
- isBitNodeFinished() &&
- !Player.augmentations.some((a) => a.name === AugmentationName.StaneksGift1),
- },
DEVMENU: {
...achievementData.DEVMENU,
Icon: "SF-1",
diff --git a/src/Achievements/README.md b/src/Achievements/README.md
index 738b6b9b8..28052e947 100644
--- a/src/Achievements/README.md
+++ b/src/Achievements/README.md
@@ -23,5 +23,7 @@ Steps:
- Match the order of achievements in `AchievementData.json`.
- `Icon` must be the name of the .svg file.
- `NotInSteam` must be true.
-- Run `pack-for-web.sh`.
+- Run `pack-for-web.sh` in `$PROJECT_DIR/assets/Steam/achievements`.
- When committing, remember to commit the changes in `$PROJECT_DIR/dist/icons/achievements`.
+
+Note: If you add a new SFx.1 achievement, you must add its ID to `SFAchievementIds` in `$PROJECT_DIR/src/Achievements/Types.ts`.
diff --git a/src/Achievements/Types.ts b/src/Achievements/Types.ts
new file mode 100644
index 000000000..6105f7368
--- /dev/null
+++ b/src/Achievements/Types.ts
@@ -0,0 +1,20 @@
+import data from "./AchievementData.json";
+
+export type AchievementId = keyof typeof data.achievements;
+export const SFAchievementIds = [
+ "SF1.1",
+ "SF2.1",
+ "SF3.1",
+ "SF4.1",
+ "SF5.1",
+ "SF6.1",
+ "SF7.1",
+ "SF8.1",
+ "SF9.1",
+ "SF10.1",
+ "SF11.1",
+ "SF12.1",
+ "SF13.1",
+ "SF14.1",
+] as const;
+export type SFAchievementId = (typeof SFAchievementIds)[number];
diff --git a/src/BitNode/BitNodeUtils.ts b/src/BitNode/BitNodeUtils.ts
index e36957145..c21d25115 100644
--- a/src/BitNode/BitNodeUtils.ts
+++ b/src/BitNode/BitNodeUtils.ts
@@ -4,8 +4,7 @@ import { GetServer } from "../Server/AllServers";
import { Server } from "../Server/Server";
import { SpecialServers } from "../Server/data/SpecialServers";
import { JSONMap } from "../Types/Jsonable";
-
-export const validBitNodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
+import { validBitNodes } from "./Constants";
export function isBitNodeFinished(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon);
diff --git a/src/BitNode/Constants.ts b/src/BitNode/Constants.ts
new file mode 100644
index 000000000..1fbe265cd
--- /dev/null
+++ b/src/BitNode/Constants.ts
@@ -0,0 +1 @@
+export const validBitNodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
diff --git a/src/DevMenu/ui/AchievementsDev.tsx b/src/DevMenu/ui/AchievementsDev.tsx
index 057c50cc4..98e5c6901 100644
--- a/src/DevMenu/ui/AchievementsDev.tsx
+++ b/src/DevMenu/ui/AchievementsDev.tsx
@@ -14,11 +14,12 @@ import LockOpenIcon from "@mui/icons-material/LockOpen";
import { Player } from "@player";
import { achievements } from "../../Achievements/Achievements";
import { Engine } from "../../engine";
+import type { AchievementId } from "../../Achievements/Types";
export function AchievementsDev(): React.ReactElement {
const [playerAchievement, setPlayerAchievements] = useState(Player.achievements.map((m) => m.ID));
- function grantAchievement(id: string): void {
+ function grantAchievement(id: AchievementId): void {
Player.giveAchievement(id);
setPlayerAchievements(Player.achievements.map((m) => m.ID));
}
@@ -28,7 +29,7 @@ export function AchievementsDev(): React.ReactElement {
setPlayerAchievements(Player.achievements.map((m) => m.ID));
}
- function removeAchievement(id: string): void {
+ function removeAchievement(id: AchievementId): void {
Player.achievements = Player.achievements.filter((a) => a.ID !== id);
setPlayerAchievements(Player.achievements.map((m) => m.ID));
}
diff --git a/src/Go/Go.ts b/src/Go/Go.ts
index 3c887a6ee..eebd25b43 100644
--- a/src/Go/Go.ts
+++ b/src/Go/Go.ts
@@ -16,6 +16,8 @@ export class GoObject {
currentGame: BoardState = getNewBoardState(7);
stats: PartialRecord = {};
storedCycles: number = 0;
+ // This flag is used when checking the achievement CHALLENGE_BN14.
+ moveOrCheatViaApi = false;
prestigeAugmentation() {
for (const opponent of getRecordKeys(Go.stats)) {
@@ -36,6 +38,7 @@ export class GoObject {
this.previousGame = null;
this.currentGame = getNewBoardState(7);
this.stats = {};
+ this.moveOrCheatViaApi = false;
resetGoPromises();
}
diff --git a/src/Go/SaveLoad.ts b/src/Go/SaveLoad.ts
index 1950b6ac2..39c6eef28 100644
--- a/src/Go/SaveLoad.ts
+++ b/src/Go/SaveLoad.ts
@@ -24,6 +24,7 @@ type SaveFormat = {
currentGame: CurrentGameSaveData;
stats: PartialRecord;
storedCycles: number;
+ moveOrCheatViaApi: boolean;
};
export function getGoSave(): SaveFormat {
@@ -46,6 +47,7 @@ export function getGoSave(): SaveFormat {
},
stats: Go.stats,
storedCycles: Go.storedCycles,
+ moveOrCheatViaApi: Go.moveOrCheatViaApi,
};
}
@@ -85,6 +87,9 @@ export function loadGo(data: unknown): boolean {
Go.previousGame = previousGame;
Go.stats = stats;
Go.storeCycles(loadStoredCycles(parsedData.storedCycles));
+ if (typeof parsedData.moveOrCheatViaApi === "boolean") {
+ Go.moveOrCheatViaApi = parsedData.moveOrCheatViaApi;
+ }
resetAI();
handleNextTurn(currentGame).catch((error) => {
diff --git a/src/Go/boardAnalysis/scoring.ts b/src/Go/boardAnalysis/scoring.ts
index 6f1f73b0e..967f48a85 100644
--- a/src/Go/boardAnalysis/scoring.ts
+++ b/src/Go/boardAnalysis/scoring.ts
@@ -1,7 +1,7 @@
import type { Board, BoardState, PointState } from "../Types";
import { Player } from "@player";
-import { GoOpponent, GoColor } from "@enums";
+import { GoOpponent, GoColor, FactionName } from "@enums";
import { newOpponentStats } from "../Constants";
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
import { getKomi, resetAI } from "./goAI";
@@ -77,6 +77,10 @@ export function endGoGame(boardState: BoardState) {
Factions[factionName].setFavor(newFavor);
statusToUpdate.rep += repToAdd;
}
+
+ if (factionName === FactionName.Illuminati && statusToUpdate.winStreak >= 10) {
+ Player.giveAchievement("IPVGO_WINNING_STREAK");
+ }
}
statusToUpdate.nodePower +=
diff --git a/src/Go/effects/netscriptGoImplementation.ts b/src/Go/effects/netscriptGoImplementation.ts
index 74712938d..facca2b23 100644
--- a/src/Go/effects/netscriptGoImplementation.ts
+++ b/src/Go/effects/netscriptGoImplementation.ts
@@ -29,6 +29,7 @@ import { newOpponentStats } from "../Constants";
* Check the move based on the current settings
*/
export function validateMove(error: (s: string) => never, x: number, y: number, methodName = "", settings = {}): void {
+ Go.moveOrCheatViaApi = true;
const check = {
emptyNode: true,
requireNonEmptyNode: false,
@@ -502,10 +503,11 @@ export function determineCheatSuccess(
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount, playAsWhite)) {
callback();
}
- // If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
+ // If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly ending the game
else if (priorCheatCount && (ejectRngOverride ?? rng.random()) < 0.1 && state.ai !== GoOpponent.none) {
logger(`Cheat failed! You have been ejected from the subnet.`);
forceEndGoGame(state);
+ Player.giveAchievement("IPVGO_ANTICHEAT");
return handleNextTurn(state, true);
} else {
// If the cheat fails, your turn is skipped
diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts
index 300165ffb..dded0a2fa 100644
--- a/src/NetscriptFunctions.ts
+++ b/src/NetscriptFunctions.ts
@@ -108,7 +108,8 @@ import { ServerConstants } from "./Server/data/Constants";
import { assertFunctionWithNSContext } from "./Netscript/TypeAssertion";
import { Router } from "./ui/GameRoot";
import { Page } from "./ui/Router";
-import { canAccessBitNodeFeature, validBitNodes } from "./BitNode/BitNodeUtils";
+import { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils";
+import { validBitNodes } from "./BitNode/Constants";
import { isIPAddress } from "./Types/strings";
import { compile } from "./NetscriptJSEvaluator";
import { Script } from "./Script/Script";
diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts
index 59a3235e9..6800840b5 100644
--- a/src/NetscriptFunctions/Singularity.ts
+++ b/src/NetscriptFunctions/Singularity.ts
@@ -48,7 +48,7 @@ import { ServerConstants } from "../Server/data/Constants";
import { blackOpsArray } from "../Bladeburner/data/BlackOperations";
import { calculateEffectiveRequiredReputation } from "../Company/utils";
import { addRepToFavor } from "../Faction/formulas/favor";
-import { validBitNodes } from "../BitNode/BitNodeUtils";
+import { validBitNodes } from "../BitNode/Constants";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
import { cat } from "../Terminal/commands/cat";
import { Crimes } from "../Crime/Crimes";
diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts
index c5d89f9c3..a6300a080 100644
--- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts
+++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts
@@ -54,6 +54,7 @@ import { AlertEvents } from "../../ui/React/AlertManager";
import { Augmentations } from "../../Augmentation/Augmentations";
import { PlayerEventType, PlayerEvents } from "./PlayerEvents";
import { Result } from "../../types";
+import type { AchievementId } from "../../Achievements/Types";
export function init(this: PlayerObject): void {
/* Initialize Player's home computer */
@@ -561,9 +562,11 @@ export function giveExploit(this: PlayerObject, exploit: Exploit): void {
}
}
-export function giveAchievement(this: PlayerObject, achievementId: string): void {
+export function giveAchievement(this: PlayerObject, achievementId: AchievementId): void {
const achievement = achievements[achievementId];
- if (!achievement) return;
+ 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}"`, ToastVariant.SUCCESS, 2000);
diff --git a/test/jest/__snapshots__/FullSave.test.ts.snap b/test/jest/__snapshots__/FullSave.test.ts.snap
index 2215537ef..4c7c056a3 100644
--- a/test/jest/__snapshots__/FullSave.test.ts.snap
+++ b/test/jest/__snapshots__/FullSave.test.ts.snap
@@ -45,6 +45,7 @@ exports[`Check Save File Continuity GoSave continuity 1`] = `
"previousBoard": "",
"previousPlayer": "White",
},
+ "moveOrCheatViaApi": false,
"previousGame": null,
"stats": {},
"storedCycles": 0,