diff --git a/src/Constants.ts b/src/Constants.ts index 012877c65c..790d69c627 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -109,7 +109,7 @@ export const CONSTANTS = { // Also update Documentation/doc/changelog.md when appropriate (when doing a release) LatestUpdate: ` -## v2.7.0 Dev: Last updated 27 November 2024 +## v2.7.0 Dev: Last updated 28 November 2024 ### MAJOR ADDITIONS @@ -275,7 +275,8 @@ export const CONSTANTS = { - Remove WD from Hashnet server list if TRP not installed (@gmcew) - Deduct karma when successfully completing action involving killing (@catloversg) - Fix: Hashserver UI shows wrong server list when purchasing upgrades (@catloversg) -- Fix wrong initial productionMult of new division (@catloversg) +- Fix: Wrong initial productionMult of new division (@catloversg) +- Fix: Sleeves UI shows and sets wrong task (@catloversg) ### CODEBASE/REFACTOR @@ -330,5 +331,6 @@ export const CONSTANTS = { - Fix: Generic Reviver does not handle Message class (@catloversg) - Add tests for b1tflum3 and destroyW0r1dD43m0n API (@catloversg) - Multiple large refactors to savegame loading for better validation and safety (@catloversg) +- Enable new lint rules (@catloversg) `, } as const; diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx index 33250318fe..0220b773d6 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -1,6 +1,6 @@ import { Box, Button, Paper, Tooltip, Typography } from "@mui/material"; -import React, { useState } from "react"; -import { CrimeType, FactionWorkType } from "@enums"; +import React, { useEffect, useState } from "react"; +import { BladeburnerActionType, CrimeType, FactionWorkType, GymType } from "@enums"; import { CONSTANTS } from "../../../Constants"; import { Player } from "@player"; import { formatPercent, formatInt } from "../../../ui/formatNumber"; @@ -12,7 +12,7 @@ import { EarningsElement, StatsElement } from "./StatsElement"; import { TaskSelector } from "./TaskSelector"; import { TravelModal } from "./TravelModal"; import { findCrime } from "../../../Crime/CrimeHelpers"; -import { SleeveWorkType } from "../Work/Work"; +import { type SleeveWork, SleeveWorkType } from "../Work/Work"; import { getEnumHelper } from "../../../utils/EnumHelper"; function getWorkDescription(sleeve: Sleeve, progress: number): string { @@ -74,6 +74,51 @@ function getWorkDescription(sleeve: Sleeve, progress: number): string { } } +function calculateABC(work: SleeveWork | null): [string, string, string] { + if (work === null) { + return ["Idle", "------", "------"]; + } + switch (work.type) { + case SleeveWorkType.COMPANY: + return ["Work for Company", work.companyName, "------"]; + case SleeveWorkType.FACTION: { + const workNames = { + [FactionWorkType.field]: "Field Work", + [FactionWorkType.hacking]: "Hacking Contracts", + [FactionWorkType.security]: "Security Work", + }; + return ["Work for Faction", work.factionName, workNames[work.factionWorkType] ?? ""]; + } + case SleeveWorkType.BLADEBURNER: + if (work.actionId.type === BladeburnerActionType.Contract) { + return ["Perform Bladeburner Actions", "Take on contracts", work.actionId.name]; + } + return ["Perform Bladeburner Actions", work.actionId.name, "------"]; + case SleeveWorkType.CLASS: { + if (!work.isGym()) { + return ["Take University Course", work.classType, work.location]; + } + const gymNames: Record = { + [GymType.strength]: "Train Strength", + [GymType.defense]: "Train Defense", + [GymType.dexterity]: "Train Dexterity", + [GymType.agility]: "Train Agility", + }; + return ["Workout at Gym", gymNames[work.classType as GymType], work.location]; + } + case SleeveWorkType.CRIME: + return ["Commit Crime", getEnumHelper("CrimeType").getMember(work.crimeType, { alwaysMatch: true }), "------"]; + case SleeveWorkType.SUPPORT: + return ["Perform Bladeburner Actions", "Support main sleeve", "------"]; + case SleeveWorkType.INFILTRATE: + return ["Perform Bladeburner Actions", "Infiltrate Synthoids", "------"]; + case SleeveWorkType.RECOVERY: + return ["Shock Recovery", "------", "------"]; + case SleeveWorkType.SYNCHRO: + return ["Synchronize", "------", "------"]; + } +} + interface SleeveElemProps { sleeve: Sleeve; rerender: () => void; @@ -83,7 +128,19 @@ export function SleeveElem(props: SleeveElemProps): React.ReactElement { const [travelOpen, setTravelOpen] = useState(false); const [augmentationsOpen, setAugmentationsOpen] = useState(false); - const [abc, setABC] = useState(["Idle", "------", "------"]); + /** + * "abc" contains values of 3 dropdown inputs. It will be set when: + * - The player selects a task and its options. + * - The sleeve's current task is set by non-UI things (e.g., NS API). + */ + const [abc, setABC] = useState(calculateABC(props.sleeve.currentWork)); + + /** + * Update abc if the sleeve's current task is set by non-UI things. + */ + useEffect(() => { + setABC(calculateABC(props.sleeve.currentWork)); + }, [props.sleeve.currentWork]); function setTask(): void { switch (abc[0]) { @@ -169,7 +226,7 @@ export function SleeveElem(props: SleeveElemProps): React.ReactElement { - + diff --git a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx index a928106532..6bd3fcb0bc 100644 --- a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx +++ b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx @@ -1,18 +1,10 @@ import type { Sleeve } from "../Sleeve"; -import React, { useState } from "react"; +import React from "react"; import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; import { Player } from "@player"; -import { - BladeburnerActionType, - BladeburnerContractName, - CityName, - FactionName, - FactionWorkType, - GymType, - LocationName, -} from "@enums"; +import { BladeburnerActionType, BladeburnerContractName, CityName, FactionName, LocationName } from "@enums"; import { Crimes } from "../../../Crime/Crimes"; import { Factions } from "../../../Faction/Factions"; import { getEnumHelper } from "../../../utils/EnumHelper"; @@ -42,7 +34,8 @@ const bladeburnerSelectorOptions: string[] = [ interface IProps { sleeve: Sleeve; - setABC: (abc: string[]) => void; + abc: [string, string, string]; + setABC: (abc: [string, string, string]) => void; } interface ITaskDetails { @@ -247,89 +240,49 @@ const canDo: { Synchronize: (sleeve: Sleeve) => sleeve.sync < 100, }; -function getABC(sleeve: Sleeve): [string, string, string] { - const work = sleeve.currentWork; - if (work === null) return ["Idle", "------", "------"]; - switch (work.type) { - case SleeveWorkType.COMPANY: - return ["Work for Company", work.companyName, "------"]; - case SleeveWorkType.FACTION: { - const workNames = { - [FactionWorkType.field]: "Field Work", - [FactionWorkType.hacking]: "Hacking Contracts", - [FactionWorkType.security]: "Security Work", - }; - return ["Work for Faction", work.factionName, workNames[work.factionWorkType] ?? ""]; - } - case SleeveWorkType.BLADEBURNER: - if (work.actionId.type === BladeburnerActionType.Contract) { - return ["Perform Bladeburner Actions", "Take on contracts", work.actionId.name]; - } - return ["Perform Bladeburner Actions", work.actionId.name, "------"]; - case SleeveWorkType.CLASS: { - if (!work.isGym()) return ["Take University Course", work.classType, work.location]; - const gymNames: Record = { - [GymType.strength]: "Train Strength", - [GymType.defense]: "Train Defense", - [GymType.dexterity]: "Train Dexterity", - [GymType.agility]: "Train Agility", - }; - return ["Workout at Gym", gymNames[work.classType as GymType], work.location]; - } - case SleeveWorkType.CRIME: - return ["Commit Crime", getEnumHelper("CrimeType").getMember(work.crimeType, { alwaysMatch: true }), "------"]; - case SleeveWorkType.SUPPORT: - return ["Perform Bladeburner Actions", "Support main sleeve", "------"]; - case SleeveWorkType.INFILTRATE: - return ["Perform Bladeburner Actions", "Infiltrate Synthoids", "------"]; - case SleeveWorkType.RECOVERY: - return ["Shock Recovery", "------", "------"]; - case SleeveWorkType.SYNCHRO: - return ["Synchronize", "------", "------"]; - } -} - export function TaskSelector(props: IProps): React.ReactElement { - const abc = getABC(props.sleeve); - const [s0, setS0] = useState(abc[0]); - const [s1, setS1] = useState(abc[1]); - const [s2, setS2] = useState(abc[2]); + const s0 = props.abc[0]; + const s1 = props.abc[1]; + const s2 = props.abc[2]; - const validActions = Object.keys(canDo).filter((k) => (canDo[k] as (sleeve: Sleeve) => boolean)(props.sleeve)); + const validActions = Object.keys(canDo).filter((taskType) => { + const canDoTask = canDo[taskType]; + if (canDoTask === undefined) { + return false; + } + return canDoTask(props.sleeve); + }); const detailsF = tasks[s0]; - if (detailsF === undefined) throw new Error(`No function for task '${s0}'`); + if (detailsF === undefined) { + throw new Error(`No function for task '${s0}'`); + } const details = detailsF(props.sleeve); const details2 = details.second(s1); if (details.first.length > 0 && !details.first.includes(s1)) { - setS1(details.first[0]); props.setABC([s0, details.first[0], s2]); } if (details2.length > 0 && !details2.includes(s2)) { - setS2(details2[0]); props.setABC([s0, s1, details2[0]]); } function onS0Change(event: SelectChangeEvent): void { const n = event.target.value; const detailsF = tasks[n]; - if (detailsF === undefined) throw new Error(`No function for task '${s0}'`); + if (detailsF === undefined) { + throw new Error(`No function for task '${s0}'`); + } const details = detailsF(props.sleeve); const details2 = details.second(details.first[0]) ?? ["------"]; - setS2(details2[0]); - setS1(details.first[0]); - setS0(n); props.setABC([n, details.first[0], details2[0]]); } function onS1Change(event: SelectChangeEvent): void { - setS1(event.target.value); props.setABC([s0, event.target.value, s2]); } function onS2Change(event: SelectChangeEvent): void { - setS2(event.target.value); props.setABC([s0, s1, event.target.value]); }