Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUGFIX: Sleeves UI shows and sets wrong task #1807

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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;
67 changes: 62 additions & 5 deletions src/PersonObjects/Sleeve/ui/SleeveElem.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -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, string> = {
[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;
Expand All @@ -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]) {
Expand Down Expand Up @@ -169,7 +226,7 @@ export function SleeveElem(props: SleeveElemProps): React.ReactElement {
</span>
<span>
<EarningsElement sleeve={props.sleeve} />
<TaskSelector sleeve={props.sleeve} setABC={setABC} />
<TaskSelector sleeve={props.sleeve} abc={abc} setABC={setABC} />
<Button onClick={setTask} sx={{ width: "100%" }}>
Set Task
</Button>
Expand Down
87 changes: 20 additions & 67 deletions src/PersonObjects/Sleeve/ui/TaskSelector.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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, string> = {
[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]);
}

Expand Down