From 862fbd773c18d7dd83fc01b6e319b2aacf8a2d3a Mon Sep 17 00:00:00 2001
From: SkyratBot <59378654+SkyratBot@users.noreply.github.com>
Date: Mon, 4 Jul 2022 02:10:31 +0200
Subject: [PATCH] [MIRROR] Completely rewrites the orbit screen [MDB IGNORE]
(#14715)
* Completely rewrites the orbit screen (#68054)
I set about refactoring Orbit.js when I just decided to scrap it and maybe add in some different functionality.
This PR:
- rewrites orbit from the ground up in typescript
- color coded list + scrollable + wider
- adds some styling and effects to gauge "threat": icons change, buttons change color
- fixes button text overflow
- fixes issue where scrolling hides the search header
- fixes similar antags being grouped separately (nukie and nukie leader, etc)
* Completely rewrites the orbit screen
Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
---
code/modules/mob/dead/observer/orbit.dm | 5 -
tgui/packages/tgui/interfaces/Orbit.js | 242 ------------------
tgui/packages/tgui/interfaces/Orbit.tsx | 317 ++++++++++++++++++++++++
tgui/packages/tgui/interfaces/Wires.tsx | 2 +-
4 files changed, 318 insertions(+), 248 deletions(-)
delete mode 100644 tgui/packages/tgui/interfaces/Orbit.js
create mode 100644 tgui/packages/tgui/interfaces/Orbit.tsx
diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm
index eaa2012b0ac..fdcfc15b8e0 100644
--- a/code/modules/mob/dead/observer/orbit.dm
+++ b/code/modules/mob/dead/observer/orbit.dm
@@ -39,11 +39,6 @@ GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new)
update_static_data(usr, ui)
return TRUE
-/datum/orbit_menu/ui_assets()
- return list(
- get_asset_datum(/datum/asset/simple/orbit),
- )
-
/datum/orbit_menu/ui_static_data(mob/user)
var/list/new_mob_pois = SSpoints_of_interest.get_mob_pois(CALLBACK(src, .proc/validate_mob_poi), append_dead_role = FALSE)
var/list/new_other_pois = SSpoints_of_interest.get_other_pois()
diff --git a/tgui/packages/tgui/interfaces/Orbit.js b/tgui/packages/tgui/interfaces/Orbit.js
deleted file mode 100644
index 6882097ead2..00000000000
--- a/tgui/packages/tgui/interfaces/Orbit.js
+++ /dev/null
@@ -1,242 +0,0 @@
-import { createSearch } from 'common/string';
-import { multiline } from 'common/string';
-import { resolveAsset } from '../assets';
-import { useBackend, useLocalState } from '../backend';
-import { Box, Button, Divider, Flex, Icon, Input, Section } from '../components';
-import { Window } from '../layouts';
-
-const PATTERN_NUMBER = / \(([0-9]+)\)$/;
-
-const searchFor = (searchText) =>
- createSearch(searchText, (thing) => thing.name);
-
-const compareString = (a, b) => (a < b ? -1 : a > b);
-
-const compareNumberedText = (a, b) => {
- const aName = a.name;
- const bName = b.name;
-
- // Check if aName and bName are the same except for a number at the end
- // e.g. Medibot (2) and Medibot (3)
- const aNumberMatch = aName.match(PATTERN_NUMBER);
- const bNumberMatch = bName.match(PATTERN_NUMBER);
-
- if (
- aNumberMatch &&
- bNumberMatch &&
- aName.replace(PATTERN_NUMBER, '') === bName.replace(PATTERN_NUMBER, '')
- ) {
- const aNumber = parseInt(aNumberMatch[1], 10);
- const bNumber = parseInt(bNumberMatch[1], 10);
-
- return aNumber - bNumber;
- }
-
- return compareString(aName, bName);
-};
-
-const BasicSection = (props, context) => {
- const { act } = useBackend(context);
- const { searchText, source, title, autoObserve } = props;
-
- const things = source.filter(searchFor(searchText));
- things.sort(compareNumberedText);
- return (
- source.length > 0 && (
-
- {things.map((thing) => (
-
- )
- );
-};
-
-const OrbitedButton = (props, context) => {
- const { act } = useBackend(context);
- const { color, thing, autoObserve } = props;
-
- return (
-
- );
-};
-
-export const Orbit = (props, context) => {
- const { act, data } = useBackend(context);
- const { alive, antagonists, dead, ghosts, misc, npcs } = data;
-
- const [searchText, setSearchText] = useLocalState(context, 'searchText', '');
- const [autoObserve, setAutoObserve] = useLocalState(
- context,
- 'autoObserve',
- false
- );
-
- const collatedAntagonists = {};
- for (const antagonist of antagonists) {
- if (collatedAntagonists[antagonist.antag] === undefined) {
- collatedAntagonists[antagonist.antag] = [];
- }
- collatedAntagonists[antagonist.antag].push(antagonist);
- }
-
- const sortedAntagonists = Object.entries(collatedAntagonists);
- sortedAntagonists.sort((a, b) => {
- return compareString(a[0], b[0]);
- });
-
- const orbitMostRelevant = (searchText) => {
- for (const source of [
- sortedAntagonists.map(([_, antags]) => antags),
- alive,
- ghosts,
- dead,
- npcs,
- misc,
- ]) {
- const member = source
- .filter(searchFor(searchText))
- .sort(compareNumberedText)[0];
- if (member !== undefined) {
- act('orbit', {
- ref: member.ref,
- auto_observe: autoObserve,
- });
- break;
- }
- }
- };
-
- return (
-
-
-
- {antagonists.length > 0 && (
-
- {sortedAntagonists.map(([name, antags]) => (
-
- {antags
- .filter(searchFor(searchText))
- .sort(compareNumberedText)
- .map((antag) => (
-
- ))}
-
- ))}
-
- )}
-
-
- {alive
- .filter(searchFor(searchText))
- .sort(compareNumberedText)
- .map((thing) => (
-
- ))}
-
-
-
- {ghosts
- .filter(searchFor(searchText))
- .sort(compareNumberedText)
- .map((thing) => (
-
- ))}
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/tgui/packages/tgui/interfaces/Orbit.tsx b/tgui/packages/tgui/interfaces/Orbit.tsx
new file mode 100644
index 00000000000..91f25be3f27
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/Orbit.tsx
@@ -0,0 +1,317 @@
+import { useBackend, useLocalState } from '../backend';
+import { filter, sortBy } from 'common/collections';
+import { multiline } from 'common/string';
+import { Button, Collapsible, Icon, Input, Section, Stack } from '../components';
+import { Window } from '../layouts';
+import { flow } from 'common/fp';
+import { logger } from '../logging';
+
+type AntagGroup = [string, Observable[]];
+
+type Data = {
+ alive: Observable[];
+ antagonists: Observable[];
+ dead: Observable[];
+ ghosts: Observable[];
+ misc: Observable[];
+ npcs: Observable[];
+};
+
+type Observable = {
+ ref: string;
+ antag?: string;
+ name: string;
+ orbiters?: number;
+};
+
+type SectionProps = {
+ color?: string;
+ section: Observable[];
+ title: string;
+};
+
+const ANTAG2COLOR = {
+ 'Abductors': 'pink',
+ 'Ash Walkers': 'olive',
+ 'Biohazards': 'brown',
+} as const;
+
+const ANTAG2GROUP = {
+ 'Abductor Agent': 'Abductors',
+ 'Abductor Scientist': 'Abductors',
+ 'Ash Walker': 'Ash Walkers',
+ 'Blob': 'Biohazards',
+ 'Sentient Disease': 'Biohazards',
+ 'Clown Operative': 'Clown Operatives',
+ 'Clown Operative Leader': 'Clown Operatives',
+ 'Nuclear Operative': 'Nuclear Operatives',
+ 'Nuclear Operative Leader': 'Nuclear Operatives',
+ 'Space Wizard': 'Wizard Federation',
+ 'Wizard Apprentice': 'Wizard Federation',
+ 'Wizard Minion': 'Wizard Federation',
+} as const;
+
+enum THREAT {
+ None,
+ Small = 'teal',
+ Medium = 'blue',
+ Large = 'violet',
+}
+
+export const Orbit = (props, context) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+/** Controls filtering out the list of observables via search */
+const ObservableSearch = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ alive = [],
+ antagonists = [],
+ dead = [],
+ ghosts = [],
+ misc = [],
+ npcs = [],
+ } = data;
+ const [autoObserve, setAutoObserve] = useLocalState(
+ context,
+ 'autoObserve',
+ false
+ );
+ const [searchQuery, setSearchQuery] = useLocalState(
+ context,
+ 'searchQuery',
+ ''
+ );
+ /** Gets a list of Observable[], then filters the most relevant to orbit */
+ const orbitMostRelevant = (searchQuery: string): void => {
+ /** Returns the most orbited observable that matches the search. */
+ const mostRelevant: Observable = flow([
+ // Filters out anything that doesn't match search
+ filter((observable) =>
+ observable.name?.toLowerCase().includes(searchQuery?.toLowerCase())
+ ),
+ // Sorts descending by orbiters
+ sortBy((poi) => -(poi.orbiters || 0)),
+ // Makes a single Observable[] list for an easy search
+ ])([alive, antagonists, dead, ghosts, misc, npcs].flat())[0];
+ logger.log(mostRelevant);
+ if (mostRelevant !== undefined) {
+ act('orbit', {
+ ref: mostRelevant.ref,
+ auto_observe: autoObserve,
+ });
+ }
+ };
+
+ return (
+
+ );
+};
+
+/**
+ * The primary content display for points of interest.
+ * Renders a scrollable section replete with subsections for each
+ * observable group.
+ */
+const ObservableContent = (props, context) => {
+ const { data } = useBackend(context);
+ const {
+ alive = [],
+ antagonists = [],
+ dead = [],
+ ghosts = [],
+ misc = [],
+ npcs = [],
+ } = data;
+ let collatedAntagonists: AntagGroup[] = [];
+ if (antagonists.length) {
+ collatedAntagonists = collateAntagonists(antagonists);
+ }
+
+ return (
+
+ {collatedAntagonists?.map(([name, antag]) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};
+
+/**
+ * Displays a collapsible with a map of observable items.
+ * Filters the results if there is a provided search query.
+ */
+const ObservableSection = (props: SectionProps, context) => {
+ const { color = 'grey', section = [], title } = props;
+ if (!section.length) {
+ return null;
+ }
+ const [searchQuery, setSearchQuery] = useLocalState(
+ context,
+ 'searchQuery',
+ ''
+ );
+ const filteredSection: Observable[] = flow([
+ filter((poi) =>
+ poi.name?.toLowerCase().includes(searchQuery?.toLowerCase())
+ ),
+ sortBy((poi) => poi.name.toLowerCase()),
+ ])(section);
+ if (!filteredSection.length) {
+ return null;
+ }
+
+ return (
+
+
+ {filteredSection.map((poi, index) => {
+ return ;
+ })}
+
+
+ );
+};
+
+/** Renders an observable button */
+const ObservableItem = (
+ props: { color: string; item: Observable },
+ context
+) => {
+ const { act } = useBackend(context);
+ const {
+ color,
+ item: { name, orbiters, ref },
+ } = props;
+ const [autoObserve, setAutoObserve] = useLocalState(
+ context,
+ 'autoObserve',
+ false
+ );
+ const threat = getThreat(orbiters || 0);
+
+ return (
+
+ );
+};
+
+/**
+ * Collates antagonist groups into their own separate sections.
+ * Some antags are grouped together lest they be listed separately,
+ * ie: Nuclear Operatives. See: ANTAG_GROUPS.
+ */
+const collateAntagonists = (antagonists: Observable[]): AntagGroup[] => {
+ const collatedAntagonists = {};
+ for (const antagonist of antagonists) {
+ const { antag } = antagonist;
+ const resolvedName = ANTAG2GROUP[antag!] || antag;
+ if (collatedAntagonists[resolvedName] === undefined) {
+ collatedAntagonists[resolvedName] = [];
+ }
+ collatedAntagonists[resolvedName].push(antagonist);
+ }
+ const sortedAntagonists = sortBy((antagonist) => antagonist[0])(
+ Object.entries(collatedAntagonists)
+ );
+
+ return sortedAntagonists;
+};
+
+/** Takes the amount of orbiters and returns some style options */
+const getThreat = (orbiters: number): THREAT => {
+ if (!orbiters || orbiters <= 2) {
+ return THREAT.None;
+ } else if (orbiters === 3) {
+ return THREAT.Small;
+ } else if (orbiters <= 6) {
+ return THREAT.Medium;
+ } else {
+ return THREAT.Large;
+ }
+};
+
+/**
+ * Returns a string with the first letter in uppercase.
+ * Unlike capitalize(), has no effect on the other letters
+ */
+const nameToUpper = (name: string): string =>
+ name.replace(/^\w/, (c) => c.toUpperCase());
diff --git a/tgui/packages/tgui/interfaces/Wires.tsx b/tgui/packages/tgui/interfaces/Wires.tsx
index f9e621a4c78..3bfd64b552f 100644
--- a/tgui/packages/tgui/interfaces/Wires.tsx
+++ b/tgui/packages/tgui/interfaces/Wires.tsx
@@ -19,7 +19,7 @@ type Wire = {
export const Wires = (props, context) => {
const { data } = useBackend(context);
const { proper_name, status = [], wires = [] } = data;
- const dynamicHeight = 150 + wires.length * 28 + (proper_name ? 30 : 0);
+ const dynamicHeight = 150 + wires.length * 30 + (proper_name ? 30 : 0);
return (