diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts index 5bfcee85884..9aed42557dc 100644 --- a/tgui/packages/common/collections.ts +++ b/tgui/packages/common/collections.ts @@ -12,33 +12,36 @@ * If collection is 'null' or 'undefined', it will be returned "as is" * without emitting any errors (which can be useful in some cases). */ -export const filter = - (iterateeFn: (input: T, index: number, collection: T[]) => boolean) => - (collection: T[]): T[] => { - if (collection === null || collection === undefined) { - return collection; - } - if (Array.isArray(collection)) { - const result: T[] = []; - for (let i = 0; i < collection.length; i++) { - const item = collection[i]; - if (iterateeFn(item, i, collection)) { - result.push(item); - } +export const filter = ( + collection: T[], + iterateeFn: (input: T, index: number, collection: T[]) => boolean, +): T[] => { + if (collection === null || collection === undefined) { + return collection; + } + if (Array.isArray(collection)) { + const result: T[] = []; + for (let i = 0; i < collection.length; i++) { + const item = collection[i]; + if (iterateeFn(item, i, collection)) { + result.push(item); } - return result; } - throw new Error(`filter() can't iterate on type ${typeof collection}`); - }; + return result; + } + throw new Error(`filter() can't iterate on type ${typeof collection}`); +}; type MapFunction = { ( + collection: T[], iterateeFn: (value: T, index: number, collection: T[]) => U, - ): (collection: T[]) => U[]; + ): U[]; ( + collection: Record, iterateeFn: (value: T, index: K, collection: Record) => U, - ): (collection: Record) => U[]; + ): U[]; }; /** @@ -49,44 +52,30 @@ type MapFunction = { * If collection is 'null' or 'undefined', it will be returned "as is" * without emitting any errors (which can be useful in some cases). */ -export const map: MapFunction = - (iterateeFn) => - (collection: T[]): U[] => { - if (collection === null || collection === undefined) { - return collection; - } - - if (Array.isArray(collection)) { - return collection.map(iterateeFn); - } +export const map: MapFunction = (collection, iterateeFn) => { + if (collection === null || collection === undefined) { + return collection; + } - if (typeof collection === 'object') { - return Object.entries(collection).map(([key, value]) => { - return iterateeFn(value, key, collection); - }); + if (Array.isArray(collection)) { + const result: unknown[] = []; + for (let i = 0; i < collection.length; i++) { + result.push(iterateeFn(collection[i], i, collection)); } + return result; + } - throw new Error(`map() can't iterate on type ${typeof collection}`); - }; - -/** - * Given a collection, will run each element through an iteratee function. - * Will then filter out undefined values. - */ -export const filterMap = ( - collection: T[], - iterateeFn: (value: T) => U | undefined, -): U[] => { - const finalCollection: U[] = []; - - for (const value of collection) { - const output = iterateeFn(value); - if (output !== undefined) { - finalCollection.push(output); + if (typeof collection === 'object') { + const result: unknown[] = []; + for (let i in collection) { + if (Object.prototype.hasOwnProperty.call(collection, i)) { + result.push(iterateeFn(collection[i], i, collection)); + } } + return result; } - return finalCollection; + throw new Error(`map() can't iterate on type ${typeof collection}`); }; const COMPARATOR = (objA, objB) => { @@ -112,39 +101,38 @@ const COMPARATOR = (objA, objB) => { * * Iteratees are called with one argument (value). */ -export const sortBy = - (...iterateeFns: ((input: T) => unknown)[]) => - (array: T[]): T[] => { - if (!Array.isArray(array)) { - return array; - } - let length = array.length; - // Iterate over the array to collect criteria to sort it by - let mappedArray: { - criteria: unknown[]; - value: T; - }[] = []; - for (let i = 0; i < length; i++) { - const value = array[i]; - mappedArray.push({ - criteria: iterateeFns.map((fn) => fn(value)), - value, - }); - } - // Sort criteria using the base comparator - mappedArray.sort(COMPARATOR); - - // Unwrap values - const values: T[] = []; - while (length--) { - values[length] = mappedArray[length].value; - } - return values; - }; +export const sortBy = ( + array: T[], + ...iterateeFns: ((input: T) => unknown)[] +): T[] => { + if (!Array.isArray(array)) { + return array; + } + let length = array.length; + // Iterate over the array to collect criteria to sort it by + let mappedArray: { + criteria: unknown[]; + value: T; + }[] = []; + for (let i = 0; i < length; i++) { + const value = array[i]; + mappedArray.push({ + criteria: iterateeFns.map((fn) => fn(value)), + value, + }); + } + // Sort criteria using the base comparator + mappedArray.sort(COMPARATOR); -export const sort = sortBy(); + // Unwrap values + const values: T[] = []; + while (length--) { + values[length] = mappedArray[length].value; + } + return values; +}; -export const sortStrings = sortBy(); +export const sort = (array: T[]): T[] => sortBy(array); /** * Returns a range of numbers from start to end, exclusively. @@ -153,12 +141,34 @@ export const sortStrings = sortBy(); export const range = (start: number, end: number): number[] => new Array(end - start).fill(null).map((_, index) => index + start); +type ReduceFunction = { + ( + array: T[], + reducerFn: ( + accumulator: U, + currentValue: T, + currentIndex: number, + array: T[], + ) => U, + initialValue: U, + ): U; + ( + array: T[], + reducerFn: ( + accumulator: T, + currentValue: T, + currentIndex: number, + array: T[], + ) => T, + ): T; +}; + /** * A fast implementation of reduce. */ -export const reduce = (reducerFn, initialValue) => (array) => { +export const reduce: ReduceFunction = (array, reducerFn, initialValue?) => { const length = array.length; - let i; + let i: number; let result; if (initialValue === undefined) { i = 1; @@ -184,15 +194,16 @@ export const reduce = (reducerFn, initialValue) => (array) => { * is determined by the order they occur in the array. The iteratee is * invoked with one argument: value. */ -export const uniqBy = - (iterateeFn?: (value: T) => unknown) => - (array: T[]): T[] => { - const { length } = array; - const result: T[] = []; - const seen: unknown[] = iterateeFn ? [] : result; - let index = -1; - // prettier-ignore - outer: +export const uniqBy = ( + array: T[], + iterateeFn?: (value: T) => unknown, +): T[] => { + const { length } = array; + const result: T[] = []; + const seen: unknown[] = iterateeFn ? [] : result; + let index = -1; + // prettier-ignore + outer: while (++index < length) { let value: T | 0 = array[index]; const computed = iterateeFn ? iterateeFn(value) : value; @@ -214,10 +225,10 @@ export const uniqBy = result.push(value); } } - return result; - }; + return result; +}; -export const uniq = uniqBy(); +export const uniq = (array: T[]): T[] => uniqBy(array); type Zip = { [I in keyof T]: T[I] extends (infer U)[] ? U : never; @@ -247,17 +258,6 @@ export const zip = (...arrays: T): Zip => { return result; }; -/** - * This method is like "zip" except that it accepts iteratee to - * specify how grouped values should be combined. The iteratee is - * invoked with the elements of each group. - */ -export const zipWith = - (iterateeFn: (...values: T[]) => U) => - (...arrays: T[][]): U[] => { - return map((values: T[]) => iterateeFn(...values))(zip(...arrays)); - }; - const binarySearch = ( getKey: (value: T) => U, collection: readonly T[], @@ -293,13 +293,15 @@ const binarySearch = ( return compare > insertingKey ? middle : middle + 1; }; -export const binaryInsertWith = - (getKey: (value: T) => U) => - (collection: readonly T[], value: T) => { - const copy = [...collection]; - copy.splice(binarySearch(getKey, collection, value), 0, value); - return copy; - }; +export const binaryInsertWith = ( + collection: readonly T[], + value: T, + getKey: (value: T) => U, +): T[] => { + const copy = [...collection]; + copy.splice(binarySearch(getKey, collection, value), 0, value); + return copy; +}; /** * This method takes a collection of items and a number, returning a collection @@ -325,7 +327,8 @@ export const paginate = (collection: T[], maxPerPage: number): T[][] => { return pages; }; -const isObject = (obj: unknown) => typeof obj === 'object' && obj !== null; +const isObject = (obj: unknown): obj is object => + typeof obj === 'object' && obj !== null; // Does a deep merge of two objects. DO NOT FEED CIRCULAR OBJECTS!! export const deepMerge = (...objects: any[]): any => { diff --git a/tgui/packages/common/fp.js b/tgui/packages/common/fp.js index ba7df09d407..675e98d807e 100644 --- a/tgui/packages/common/fp.js +++ b/tgui/packages/common/fp.js @@ -23,27 +23,3 @@ export const flow = (...funcs) => (input, ...rest) => { } return output; }; - -/** - * Composes single-argument functions from right to left. - * - * All functions might accept a context in form of additional arguments. - * If the resulting function is called with more than 1 argument, rest of - * the arguments are passed to all functions unchanged. - * - * @param {...Function} funcs The functions to compose - * @returns {Function} A function obtained by composing the argument functions - * from right to left. For example, compose(f, g, h) is identical to doing - * (input, ...rest) => f(g(h(input, ...rest), ...rest), ...rest) - */ -export const compose = (...funcs) => { - if (funcs.length === 0) { - return (arg) => arg; - } - if (funcs.length === 1) { - return funcs[0]; - } - // prettier-ignore - return funcs.reduce((a, b) => (value, ...rest) => - a(b(value, ...rest), ...rest)); -}; diff --git a/tgui/packages/common/vector.js b/tgui/packages/common/vector.js deleted file mode 100644 index b1f85f7429d..00000000000 --- a/tgui/packages/common/vector.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * N-dimensional vector manipulation functions. - * - * Vectors are plain number arrays, i.e. [x, y, z]. - * - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -import { map, reduce, zipWith } from './collections'; - -const ADD = (a, b) => a + b; -const SUB = (a, b) => a - b; -const MUL = (a, b) => a * b; -const DIV = (a, b) => a / b; - -export const vecAdd = (...vecs) => { - return reduce((a, b) => zipWith(ADD)(a, b))(vecs); -}; - -export const vecSubtract = (...vecs) => { - return reduce((a, b) => zipWith(SUB)(a, b))(vecs); -}; - -export const vecMultiply = (...vecs) => { - return reduce((a, b) => zipWith(MUL)(a, b))(vecs); -}; - -export const vecDivide = (...vecs) => { - return reduce((a, b) => zipWith(DIV)(a, b))(vecs); -}; - -export const vecScale = (vec, n) => { - return map((x) => x * n)(vec); -}; - -export const vecInverse = (vec) => { - return map((x) => -x)(vec); -}; - -export const vecLength = (vec) => { - return Math.sqrt(reduce(ADD)(zipWith(MUL)(vec, vec))); -}; - -export const vecNormalize = (vec) => { - return vecDivide(vec, vecLength(vec)); -}; diff --git a/tgui/packages/common/vector.ts b/tgui/packages/common/vector.ts new file mode 100644 index 00000000000..c91715a8f99 --- /dev/null +++ b/tgui/packages/common/vector.ts @@ -0,0 +1,51 @@ +/** + * N-dimensional vector manipulation functions. + * + * Vectors are plain number arrays, i.e. [x, y, z]. + * + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { map, reduce, zip } from './collections'; + +const ADD = (a: number, b: number): number => a + b; +const SUB = (a: number, b: number): number => a - b; +const MUL = (a: number, b: number): number => a * b; +const DIV = (a: number, b: number): number => a / b; + +export type Vector = number[]; + +export const vecAdd = (...vecs: Vector[]): Vector => { + return map(zip(...vecs), (x) => reduce(x, ADD)); +}; + +export const vecSubtract = (...vecs: Vector[]): Vector => { + return map(zip(...vecs), (x) => reduce(x, SUB)); +}; + +export const vecMultiply = (...vecs: Vector[]): Vector => { + return map(zip(...vecs), (x) => reduce(x, MUL)); +}; + +export const vecDivide = (...vecs: Vector[]): Vector => { + return map(zip(...vecs), (x) => reduce(x, DIV)); +}; + +export const vecScale = (vec: Vector, n: number): Vector => { + return map(vec, (x) => x * n); +}; + +export const vecInverse = (vec: Vector): Vector => { + return map(vec, (x) => -x); +}; + +export const vecLength = (vec: Vector): number => { + return Math.sqrt(reduce(vecMultiply(vec, vec), ADD)); +}; + +export const vecNormalize = (vec: Vector): Vector => { + const length = vecLength(vec); + return map(vec, (c) => c / length); +}; diff --git a/tgui/packages/tgui-panel/chat/selectors.ts b/tgui/packages/tgui-panel/chat/selectors.ts index 2908f661264..3c1e0b4f429 100644 --- a/tgui/packages/tgui-panel/chat/selectors.ts +++ b/tgui/packages/tgui-panel/chat/selectors.ts @@ -9,7 +9,7 @@ import { map } from 'common/collections'; export const selectChat = (state) => state.chat; export const selectChatPages = (state) => - map((id: string) => state.chat.pageById[id])(state.chat.pages); + map(state.chat.pages, (id: string) => state.chat.pageById[id]); export const selectCurrentChatPage = (state) => state.chat.pageById[state.chat.currentPageId]; diff --git a/tgui/packages/tgui/components/Chart.tsx b/tgui/packages/tgui/components/Chart.tsx index 205dc6fbec1..bc33ff90606 100644 --- a/tgui/packages/tgui/components/Chart.tsx +++ b/tgui/packages/tgui/components/Chart.tsx @@ -4,7 +4,7 @@ * @license MIT */ -import { map, zipWith } from 'common/collections'; +import { map, zip } from 'common/collections'; import { Component, createRef, RefObject } from 'react'; import { Box, BoxProps } from './Box'; @@ -37,8 +37,8 @@ const normalizeData = ( return []; } - const min = zipWith(Math.min)(...data); - const max = zipWith(Math.max)(...data); + const min = map(zip(...data), (p) => Math.min(...p)); + const max = map(zip(...data), (p) => Math.max(...p)); if (rangeX !== undefined) { min[0] = rangeX[0]; @@ -50,11 +50,12 @@ const normalizeData = ( max[1] = rangeY[1]; } - const normalized = map((point: Point) => { - return zipWith((value: number, min: number, max: number, scale: number) => { - return ((value - min) / (max - min)) * scale; - })(point, min, max, scale); - })(data); + const normalized = map(data, (point) => + map( + zip(point, min, max, scale), + ([value, min, max, scale]) => ((value - min) / (max - min)) * scale, + ), + ); return normalized; }; diff --git a/tgui/packages/tgui/components/Popper.tsx b/tgui/packages/tgui/components/Popper.tsx index dbc7c18f7b6..11c848e7eae 100644 --- a/tgui/packages/tgui/components/Popper.tsx +++ b/tgui/packages/tgui/components/Popper.tsx @@ -20,6 +20,10 @@ type OptionalProps = Partial<{ onClickOutside: () => void; /** Where to place the popper relative to the reference element */ placement: Placement; + /** Base z-index of the popper div + * @default 5 + */ + baseZIndex: number; }>; type Props = RequiredProps & OptionalProps; @@ -85,7 +89,7 @@ export function Popper(props: PropsWithChildren) { setPopperElement(node); popperRef.current = node; }} - style={{ ...styles.popper, zIndex: 5 }} + style={{ ...styles.popper, zIndex: props.baseZIndex ?? 5 }} {...attributes.popper} > {content} diff --git a/tgui/packages/tgui/drag.ts b/tgui/packages/tgui/drag.ts index 584666a97a6..0884b1b0bd7 100644 --- a/tgui/packages/tgui/drag.ts +++ b/tgui/packages/tgui/drag.ts @@ -209,7 +209,7 @@ export const dragStartHandler = (event) => { dragPointOffset = vecSubtract( [event.screenX, event.screenY], getWindowPosition(), - ); + ) as [number, number]; // Focus click target (event.target as HTMLElement)?.focus(); document.addEventListener('mousemove', dragMoveHandler); @@ -234,7 +234,10 @@ const dragMoveHandler = (event: MouseEvent) => { } event.preventDefault(); setWindowPosition( - vecSubtract([event.screenX, event.screenY], dragPointOffset), + vecSubtract([event.screenX, event.screenY], dragPointOffset) as [ + number, + number, + ], ); }; @@ -247,7 +250,7 @@ export const resizeStartHandler = dragPointOffset = vecSubtract( [event.screenX, event.screenY], getWindowPosition(), - ); + ) as [number, number]; initialSize = getWindowSize(); // Focus click target (event.target as HTMLElement)?.focus(); @@ -278,7 +281,10 @@ const resizeMoveHandler = (event: MouseEvent) => { ); const delta = vecSubtract(currentOffset, dragPointOffset); // Extra 1x1 area is added to ensure the browser can see the cursor - size = vecAdd(initialSize, vecMultiply(resizeMatrix, delta), [1, 1]); + size = vecAdd(initialSize, vecMultiply(resizeMatrix, delta), [1, 1]) as [ + number, + number, + ]; // Sane window size values size[0] = Math.max(size[0], 150 * pixelRatio); size[1] = Math.max(size[1], 50 * pixelRatio); diff --git a/tgui/packages/tgui/interfaces/Orbit/helpers.ts b/tgui/packages/tgui/interfaces/Orbit/helpers.ts index 8ad071c699b..c0668dd02d8 100644 --- a/tgui/packages/tgui/interfaces/Orbit/helpers.ts +++ b/tgui/packages/tgui/interfaces/Orbit/helpers.ts @@ -1,5 +1,4 @@ import { filter, sortBy } from 'common/collections'; -import { flow } from 'common/fp'; import { HEALTH, THREAT } from './constants'; import type { AntagGroup, Antagonist, Observable } from './types'; @@ -18,7 +17,7 @@ export const getAntagCategories = (antagonists: Antagonist[]) => { categories[antag_group].push(player); }); - return sortBy(([key]) => key)(Object.entries(categories)); + return sortBy(Object.entries(categories), ([key]) => key); }; /** Returns a disguised name in case the person is wearing someone else's ID */ @@ -43,15 +42,19 @@ export const getMostRelevant = ( searchQuery: string, observables: Observable[][], ): Observable => { - return flow([ - // Filters out anything that doesn't match search - filter((observable) => - isJobOrNameMatch(observable, searchQuery), - ), + const queriedObservables = // Sorts descending by orbiters - sortBy((observable) => -(observable.orbiters || 0)), - // Makes a single Observables list for an easy search - ])(observables.flat())[0]; + sortBy( + // Filters out anything that doesn't match search + filter( + observables + // Makes a single Observables list for an easy search + .flat(), + (observable) => isJobOrNameMatch(observable, searchQuery), + ), + (observable) => -(observable.orbiters || 0), + ); + return queriedObservables[0]; }; /** Returns the display color for certain health percentages */ diff --git a/tgui/packages/tgui/interfaces/Orbit/index.tsx b/tgui/packages/tgui/interfaces/Orbit/index.tsx index 0d992d981a8..0103fe8750f 100644 --- a/tgui/packages/tgui/interfaces/Orbit/index.tsx +++ b/tgui/packages/tgui/interfaces/Orbit/index.tsx @@ -1,5 +1,4 @@ import { filter, sortBy } from 'common/collections'; -import { flow } from 'common/fp'; import { capitalizeFirst, multiline } from 'common/string'; import { useBackend, useLocalState } from 'tgui/backend'; import { @@ -204,16 +203,13 @@ const ObservableSection = (props: { const [searchQuery] = useLocalState('searchQuery', ''); - const filteredSection: Observable[] = flow([ - filter((observable) => - isJobOrNameMatch(observable, searchQuery), - ), - sortBy((observable) => + const filteredSection = sortBy( + filter(section, (observable) => isJobOrNameMatch(observable, searchQuery)), + (observable) => getDisplayName(observable.full_name, observable.name) .replace(/^"/, '') .toLowerCase(), - ), - ])(section); + ); if (!filteredSection.length) { return null; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx index 2ff85058da6..c00889536e0 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/AntagsPage.tsx @@ -25,9 +25,10 @@ const antagsByCategory = new Map(); // This will break at priorities higher than 10, but that almost definitely // will not happen. -const binaryInsertAntag = binaryInsertWith((antag: Antagonist) => { - return `${antag.priority}_${antag.name}`; -}); +const binaryInsertAntag = (collection: Antagonist[], value: Antagonist) => + binaryInsertWith(collection, value, (antag) => { + return `${antag.priority}_${antag.name}`; + }); for (const antagKey of requireAntag.keys()) { const antag = requireAntag<{ diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx index df488641efa..d69b705c73e 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferenceWindow.tsx @@ -62,7 +62,7 @@ export const CharacterPreferenceWindow = (props) => { break; case Page.Main: pageContents = ( - setCurrentPage(Page.Species)} /> + ); break; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx index 05845ffe548..ad81b2c27fd 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/GamePreferencesPage.tsx @@ -13,11 +13,13 @@ type PreferenceChild = { children: ReactNode; }; -const binaryInsertPreference = binaryInsertWith( - (child) => child.name, -); +const binaryInsertPreference = ( + collection: PreferenceChild[], + value: PreferenceChild, +) => binaryInsertWith(collection, value, (child) => child.name); -const sortByName = sortBy<[string, PreferenceChild[]]>(([name]) => name); +const sortByName = (array: [string, PreferenceChild[]][]) => + sortBy(array, ([name]) => name); export const GamePreferencesPage = (props) => { const { act, data } = useBackend(); diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/JobsPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/JobsPage.tsx index ca620060e5a..cdff8ff0ce6 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/JobsPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/JobsPage.tsx @@ -17,10 +17,11 @@ import { import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; const sortJobs = (entries: [string, Job][], head?: string) => - sortBy<[string, Job]>( + sortBy( + entries, ([key, _]) => (key === head ? -1 : 1), ([key, _]) => key, - )(entries); + ); const PRIORITY_BUTTON_SIZE = '18px'; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx index 28d3c38dce0..1cacab08002 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/MainPage.tsx @@ -1,5 +1,6 @@ -import { filterMap, sortBy } from 'common/collections'; +import { filter, map, sortBy } from 'common/collections'; import { classes } from 'common/react'; +import { createSearch } from 'common/string'; import { useState } from 'react'; import { sendAct, useBackend } from '../../backend'; @@ -78,6 +79,7 @@ const ChoicedSelection = (props: { const { act } = useBackend(); const { catalog, supplementalFeature, supplementalValue } = props; + const [getSearchText, searchTextSet] = useState(''); if (!catalog.icons) { return Provided catalog had no icons!; @@ -174,6 +176,17 @@ const ChoicedSelection = (props: { ); }; +const searchInCatalog = (searchText = '', catalog: Record) => { + let items = Object.entries(catalog); + if (searchText) { + items = filter( + items, + createSearch(searchText, ([name, _icon]) => name), + ); + } + return items; +}; + const GenderButton = (props: { handleSetGender: (gender: Gender) => void; gender: Gender; @@ -334,10 +347,11 @@ const createSetRandomization = }); }; -const sortPreferences = sortBy<[string, unknown]>(([featureId, _]) => { - const feature = features[featureId]; - return feature?.name; -}); +const sortPreferences = (array: [string, unknown][]) => + sortBy(array, ([featureId, _]) => { + const feature = features[featureId]; + return feature?.name; + }); export const PreferenceList = (props: { act: typeof sendAct; @@ -417,22 +431,20 @@ export const getRandomization = ( const { data } = useBackend(); - return Object.fromEntries( - filterMap(Object.keys(preferences), (preferenceKey) => { - if (serverData.random.randomizable.indexOf(preferenceKey) === -1) { - return undefined; - } - - if (!randomBodyEnabled) { - return undefined; - } + if (!randomBodyEnabled) { + return {}; + } - return [ - preferenceKey, - data.character_preferences.randomization[preferenceKey] || - RandomSetting.Disabled, - ]; - }), + return Object.fromEntries( + map( + filter(Object.keys(preferences), (key) => + serverData.random.randomizable.includes(key), + ), + (key) => [ + key, + data.character_preferences.randomization[key] || RandomSetting.Disabled, + ], + ), ); }; @@ -513,7 +525,7 @@ export const MainPage = () => { )} - + { - + {mainFeatures.map(([clothingKey, clothing]) => { const catalog = @@ -574,6 +586,13 @@ export const MainPage = () => { setCurrentClothingMenu(clothingKey); }} handleSelect={createSetPreference(act, clothingKey)} + randomization={ + randomizationOfMainFeatures[clothingKey] + } + setRandomization={createSetRandomization( + act, + clothingKey, + )} /> ) diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx index c145b7eb3db..2dde5ab60cc 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/QuirksPage.tsx @@ -1,4 +1,4 @@ -import { filterMap } from 'common/collections'; +import { filter } from 'common/collections'; import { useState } from 'react'; import { useBackend } from '../../backend'; @@ -23,13 +23,9 @@ function getCorrespondingPreferences( relevant_preferences: Record, ) { return Object.fromEntries( - filterMap(Object.keys(relevant_preferences), (key) => { - if (!customization_options.includes(key)) { - return undefined; - } - - return [key, relevant_preferences[key]]; - }), + filter(Object.entries(relevant_preferences), ([key, value]) => + customization_options.includes(key), + ), ); } diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx index eadb6e94d42..039fb8027ad 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/names.tsx @@ -21,9 +21,11 @@ type NameWithKey = { name: Name; }; -const binaryInsertName = binaryInsertWith(({ key }) => key); +const binaryInsertName = (collection: NameWithKey[], value: NameWithKey) => + binaryInsertWith(collection, value, ({ key }) => key); -const sortNameWithKeyEntries = sortBy<[string, NameWithKey[]]>(([key]) => key); +const sortNameWithKeyEntries = (array: [string, NameWithKey[]][]) => + sortBy(array, ([key]) => key); export const MultiNameInput = (props: { handleClose: () => void; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx index 0a6ed46915a..98f43c5525d 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/base.tsx @@ -1,4 +1,4 @@ -import { sortBy, sortStrings } from 'common/collections'; +import { sort, sortBy } from 'common/collections'; import { BooleanLike, classes } from 'common/react'; import { ComponentType, @@ -21,7 +21,8 @@ import { import { createSetPreference, PreferencesMenuData } from '../../data'; import { ServerPreferencesFetcher } from '../../ServerPreferencesFetcher'; -export const sortChoices = sortBy<[string, ReactNode]>(([name]) => name); +export const sortChoices = (array: [string, ReactNode][]) => + sortBy(array, ([name]) => name); export type Feature< TReceiving, @@ -209,7 +210,7 @@ export const FeatureDropdownInput = ( return ( , }; -const sortHexValues - = sortBy<[string, HexValue]>(([_, hexValue]) => -hexValue.lightness); +const sortHexValues = (array: [string, HexValue][]) => + sortBy(array, ([_, hexValue]) => { + return -hexValue.lightness; + }); export const skin_tone: Feature = { name: "Skin tone", @@ -42,7 +45,7 @@ export const skin_tone: Feature = { diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx index 3f92ac580b0..26aa17011a6 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/admin.tsx @@ -1,4 +1,4 @@ -import { CheckboxInput, FeatureColorInput, Feature, FeatureDropdownInput, FeatureToggle } from "../base"; +import { CheckboxInput, Feature, FeatureColorInput, FeatureDropdownInput, FeatureToggle } from "../base"; export const asaycolor: Feature = { name: "Admin chat color", diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx index d51637036ec..1cccc955434 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/ghost.tsx @@ -22,10 +22,13 @@ export const ghost_accs: FeatureChoiced = { component: FeatureDropdownInput, }; -const insertGhostForm = binaryInsertWith<{ +type GhostForm = { displayText: ReactNode; value: string; -}>(({ value }) => value); +}; + +const insertGhostForm = (collection: GhostForm[], value: GhostForm) => + binaryInsertWith(collection, value, ({ value }) => value); const GhostFormInput = ( props: FeatureValueProps, diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts index 6721ee70cf5..adc044899de 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/useRandomToggleState.ts @@ -1,4 +1,3 @@ -import { useLocalState } from "../../backend"; +import { useLocalState } from '../../backend'; -export const useRandomToggleState - = context => useLocalState(context, "randomToggle", false); +export const useRandomToggleState = () => useLocalState('randomToggle', false); diff --git a/tgui/packages/tgui/interfaces/Radio.jsx b/tgui/packages/tgui/interfaces/Radio.jsx index a6a35739d43..826fa7dc4e4 100644 --- a/tgui/packages/tgui/interfaces/Radio.jsx +++ b/tgui/packages/tgui/interfaces/Radio.jsx @@ -23,10 +23,10 @@ export const Radio = (props) => { const tunedChannel = RADIO_CHANNELS.find( (channel) => channel.freq === frequency, ); - const channels = map((value, key) => ({ + const channels = map(data.channels, (value, key) => ({ name: key, status: !!value, - }))(data.channels); + })); // Calculate window height let height = 106; if (subspace) { diff --git a/tgui/packages/tgui/interfaces/SelectEquipment.jsx b/tgui/packages/tgui/interfaces/SelectEquipment.jsx index b7017a92236..f52a1e0a5a8 100644 --- a/tgui/packages/tgui/interfaces/SelectEquipment.jsx +++ b/tgui/packages/tgui/interfaces/SelectEquipment.jsx @@ -1,5 +1,4 @@ import { filter, map, sortBy, uniq } from 'common/collections'; -import { flow } from 'common/fp'; import { createSearch } from 'common/string'; import { useBackend, useLocalState } from '../backend'; @@ -20,10 +19,10 @@ export const SelectEquipment = (props) => { const isFavorited = (entry) => favorites?.includes(entry.path); - const outfits = map((entry) => ({ + const outfits = map([...data.outfits, ...data.custom_outfits], (entry) => ({ ...entry, favorite: isFavorited(entry), - }))([...data.outfits, ...data.custom_outfits]); + })); // even if no custom outfits were sent, we still want to make sure there's // at least a 'Custom' tab so the button to create a new one pops up @@ -39,15 +38,15 @@ export const SelectEquipment = (props) => { (entry) => entry.name + entry.path, ); - const visibleOutfits = flow([ - filter((entry) => entry.category === tab), - filter(searchFilter), - sortBy( - (entry) => !entry.favorite, - (entry) => !entry.priority, - (entry) => entry.name, + const visibleOutfits = sortBy( + filter( + filter(outfits, (entry) => entry.category === tab), + searchFilter, ), - ])(outfits); + (entry) => !entry.favorite, + (entry) => !entry.priority, + (entry) => entry.name, + ); const getOutfitEntry = (current_outfit) => outfits.find((outfit) => getOutfitKey(outfit) === current_outfit); diff --git a/tgui/packages/tgui/interfaces/SurgeryInitiator.tsx b/tgui/packages/tgui/interfaces/SurgeryInitiator.tsx index f48915068d9..62d71d614db 100644 --- a/tgui/packages/tgui/interfaces/SurgeryInitiator.tsx +++ b/tgui/packages/tgui/interfaces/SurgeryInitiator.tsx @@ -19,7 +19,8 @@ type SurgeryInitiatorData = { target_name: string; }; -const sortSurgeries = sortBy((surgery: Surgery) => surgery.name); +const sortSurgeries = (array: Surgery[]) => + sortBy(array, (surgery) => surgery.name); type SurgeryInitiatorInnerState = { selectedSurgeryIndex: number; diff --git a/tgui/packages/tgui/interfaces/TrackedPlaytime.jsx b/tgui/packages/tgui/interfaces/TrackedPlaytime.jsx index 55eae8d2558..88ad673f774 100644 --- a/tgui/packages/tgui/interfaces/TrackedPlaytime.jsx +++ b/tgui/packages/tgui/interfaces/TrackedPlaytime.jsx @@ -7,7 +7,7 @@ import { Window } from '../layouts'; const JOB_REPORT_MENU_FAIL_REASON_TRACKING_DISABLED = 1; const JOB_REPORT_MENU_FAIL_REASON_NO_RECORDS = 2; -const sortByPlaytime = sortBy(([_, playtime]) => -playtime); +const sortByPlaytime = (array) => sortBy(array, ([_, playtime]) => -playtime); const PlaytimeSection = (props) => { const { playtimes } = props;