diff --git a/tgui/packages/common/math.js b/tgui/packages/common/math.js
index f2918c5c111..cc7a309563e 100644
--- a/tgui/packages/common/math.js
+++ b/tgui/packages/common/math.js
@@ -2,13 +2,15 @@
* Limits a number to the range between 'min' and 'max'.
*/
export const clamp = (value, min, max) => {
- return Math.max(min, Math.min(value, max));
+ return value < min ? min : value > max ? max : value;
};
/**
* Limits a number between 0 and 1.
*/
-export const clamp01 = value => clamp(value, 0, 1);
+export const clamp01 = value => {
+ return value < 0 ? 0 : value > 1 ? 1 : value;
+};
/**
* Scales a number to fit into the range between min and max.
@@ -18,10 +20,35 @@ export const scale = (value, min, max) => {
};
/**
- * Returns a rounded number.
- * TODO: Replace this native rounding function with a more robust one.
+ * Robust number rounding.
+ *
+ * Adapted from Locutus, see: http://locutus.io/php/math/round/
+ *
+ * @param {number} value
+ * @param {number} precision
+ * @return {number}
*/
-export const round = value => Math.round(value);
+export const round = (value, precision) => {
+ if (!value || isNaN(value)) {
+ return value;
+ }
+ // helper variables
+ let m, f, isHalf, sgn;
+ // making sure precision is integer
+ precision |= 0;
+ m = Math.pow(10, precision);
+ value *= m;
+ // sign of the number
+ sgn = (value > 0) | -(value < 0);
+ // isHalf = value % 1 === 0.5 * sgn;
+ isHalf = Math.abs(value % 1) >= 0.4999999999854481;
+ f = Math.floor(value);
+ if (isHalf) {
+ // rounds .5 away from zero
+ value = f + (sgn > 0);
+ }
+ return (isHalf ? value : Math.round(value)) / m;
+};
/**
* Returns a string representing a number in fixed point notation.
diff --git a/tgui/packages/tgui/components/ProgressBar.js b/tgui/packages/tgui/components/ProgressBar.js
index db3ada7023a..5cc8a88bf29 100644
--- a/tgui/packages/tgui/components/ProgressBar.js
+++ b/tgui/packages/tgui/components/ProgressBar.js
@@ -1,4 +1,4 @@
-import { clamp, keyOfMatchingRange, toFixed } from 'common/math';
+import { clamp01, scale, keyOfMatchingRange, toFixed } from 'common/math';
import { classes, pureComponentHooks } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
@@ -13,7 +13,7 @@ export const ProgressBar = props => {
children,
...rest
} = props;
- const scaledValue = (value - minValue) / (maxValue - minValue);
+ const scaledValue = scale(value, minValue, maxValue);
const hasContent = children !== undefined;
const effectiveColor = color
|| keyOfMatchingRange(value, ranges)
@@ -28,9 +28,9 @@ export const ProgressBar = props => {
])}
{...computeBoxProps(rest)}>
{hasContent
diff --git a/tgui/packages/tgui/components/Tabs.js b/tgui/packages/tgui/components/Tabs.js
index da2bfd903d5..ff42afdc149 100644
--- a/tgui/packages/tgui/components/Tabs.js
+++ b/tgui/packages/tgui/components/Tabs.js
@@ -1,139 +1,51 @@
-import { classes, normalizeChildren } from 'common/react';
-import { Component } from 'inferno';
-import { Box } from './Box';
+import { classes } from 'common/react';
+import { computeBoxClassName, computeBoxProps } from './Box';
import { Button } from './Button';
-// A magic value for enforcing type safety
-const TAB_MAGIC_TYPE = 'Tab';
-
-const validateTabs = tabs => {
- for (let tab of tabs) {
- if (!tab.props || tab.props.__type__ !== TAB_MAGIC_TYPE) {
- const json = JSON.stringify(tab, null, 2);
- throw new Error('
only accepts children of type .'
- + 'This is what we received: ' + json);
- }
- }
+export const Tabs = props => {
+ const {
+ className,
+ vertical,
+ children,
+ ...rest
+ } = props;
+ return (
+
+ );
};
-export class Tabs extends Component {
- constructor(props) {
- super(props);
- this.state = {
- activeTabKey: null,
- };
- }
-
- getActiveTab() {
- const { state, props } = this;
- const tabs = normalizeChildren(props.children);
- validateTabs(tabs);
- // Get active tab
- let activeTabKey = props.activeTab || state.activeTabKey;
- // Verify that active tab exists
- let activeTab = tabs
- .find(tab => {
- const key = tab.key || tab.props.label;
- return key === activeTabKey;
- });
- // Set first tab as the active tab
- if (!activeTab) {
- activeTab = tabs[0];
- activeTabKey = activeTab && (activeTab.key || activeTab.props.label);
- }
- return {
- tabs,
- activeTab,
- activeTabKey,
- };
- }
-
- render() {
- const { props } = this;
- const {
- className,
- vertical,
- altSelection,
- children,
- ...rest
- } = props;
- const {
- tabs,
- activeTab,
- activeTabKey,
- } = this.getActiveTab();
- // Retrieve tab content
- let content = null;
- if (activeTab) {
- content = activeTab.props.content || activeTab.props.children;
- }
- // Get children by calling a wrapper function
- if (typeof content === 'function') {
- content = content(activeTabKey);
- }
- return (
-
-
- {tabs.map(tab => {
- const {
- className,
- label,
- content, // ignored
- children, // ignored
- onClick,
- highlight,
- ...rest
- } = tab.props;
- const key = tab.key || tab.props.label;
- const active = tab.active || key === activeTabKey;
- const altSelectionStyle = 'Button--altSelected'
- + (vertical ? '--right' : '--bottom');
- return (
-
- );
- })}
-
-
- {content || null}
-
-
- );
- }
-}
-
-/**
- * A dummy component, which is used for carrying props for the
- * tab container.
- */
-export const Tab = props => null;
-
-Tab.defaultProps = {
- __type__: TAB_MAGIC_TYPE,
+const Tab = props => {
+ const {
+ className,
+ selected,
+ altSelection,
+ ...rest
+ } = props;
+ return (
+
+ );
};
Tabs.Tab = Tab;
diff --git a/tgui/packages/tgui/format.js b/tgui/packages/tgui/format.js
index 6b0558b8f3a..1518d14c392 100644
--- a/tgui/packages/tgui/format.js
+++ b/tgui/packages/tgui/format.js
@@ -1,4 +1,4 @@
-import { clamp, toFixed } from 'common/math';
+import { clamp, round, toFixed } from 'common/math';
const SI_SYMBOLS = [
'f', // femto
@@ -54,3 +54,12 @@ const formatSiUnit = (value, minBase1000 = -SI_BASE_INDEX, unit = '') => {
export const formatPower = (value, minBase1000 = 0) => {
return formatSiUnit(value, minBase1000, 'W');
};
+
+export const formatMoney = (value, precision = 0) => {
+ return round(value, precision)
+ .toLocaleString('en', {
+ minimumFractionDigits: precision,
+ })
+ // Thin space
+ .replace(/,/g, '\u2009');
+};
diff --git a/tgui/packages/tgui/index.js b/tgui/packages/tgui/index.js
index 645d8691a0b..769820b9f58 100644
--- a/tgui/packages/tgui/index.js
+++ b/tgui/packages/tgui/index.js
@@ -3,7 +3,11 @@ import 'core-js/web/immediate';
import 'core-js/web/queue-microtask';
import 'core-js/web/timers';
import 'regenerator-runtime/runtime';
-import './polyfills';
+
+// This one is necessary for Inferno.
+if (!window.Int32Array) {
+ window.Int32Array = Array;
+}
import { loadCSS } from 'fg-loadcss';
import { render } from 'inferno';
diff --git a/tgui/packages/tgui/interfaces/Achievements.js b/tgui/packages/tgui/interfaces/Achievements.js
index 50d00b0fe82..0f6a224893f 100644
--- a/tgui/packages/tgui/interfaces/Achievements.js
+++ b/tgui/packages/tgui/interfaces/Achievements.js
@@ -1,15 +1,17 @@
-import { useBackend } from '../backend';
+import { useBackend, useLocalState } from '../backend';
import { Box, Icon, Table, Tabs } from '../components';
import { Window } from '../layouts';
+import { Fragment } from 'inferno';
export const Achievements = (props, context) => {
const { data } = useBackend(context);
- const {
- achievements,
- categories,
- highscore,
- user_ckey,
- } = data;
+ const { categories } = data;
+ const [
+ selectedCategory,
+ setSelectedCategory,
+ ] = useLocalState(context, 'category', categories[0]);
+ const achievements = data.achievements
+ .filter(x => x.category === selectedCategory);
return (
@@ -17,81 +19,40 @@ export const Achievements = (props, context) => {
{categories.map(category => (
-
- {achievements
- .filter(x => x.category === category)
- .map(achievement => {
- if (achievement.score) {
- return (
-
- );
- }
- return (
-
- );
- })}
-
+ selected={selectedCategory === category}
+ onClick={() => setSelectedCategory(category)}>
+ {category}
))}
-
- {highscore.map(highscore => (
-
-
-
-
- #
-
-
- Key
-
-
- Score
-
-
- {Object.keys(highscore.scores).map((key, index) => (
-
-
- {index + 1}
-
-
- {index === 0 && (
-
- )}
- {key}
- {index === 0 && (
-
- )}
-
-
- {highscore.scores[key]}
-
-
- ))}
-
-
- ))}
-
+ selected={selectedCategory === 'High Scores'}
+ onClick={() => setSelectedCategory('High Scores')}>
+ High Scores
+ {selectedCategory === 'High Scores' && (
+
+ ) || (
+
+ )}
);
};
+const AchievementTable = (props, context) => {
+ const { achievements } = props;
+ return (
+
+ {achievements.map(achievement => (
+
+ ))}
+
+ );
+};
+
const Achievement = props => {
const { achievement } = props;
const {
@@ -99,6 +60,7 @@ const Achievement = props => {
desc,
icon_class,
value,
+ score,
} = achievement;
return (
@@ -108,34 +70,89 @@ const Achievement = props => {
{name}
{desc}
-
- {value ? 'Unlocked' : 'Locked'}
-
+ {score && (
+ 0 ? 'good' : 'bad'}>
+ {value > 0 ? `Earned ${value} times` : 'Locked'}
+
+ ) || (
+
+ {value ? 'Unlocked' : 'Locked'}
+
+ )}
|
);
};
-const Score = props => {
- const { achievement } = props;
+const HighScoreTable = (props, context) => {
+ const { data } = useBackend(context);
const {
- name,
- desc,
- icon_class,
- value,
- } = achievement;
+ highscore: highscores,
+ user_ckey,
+ } = data;
+ const [
+ highScoreIndex,
+ setHighScoreIndex,
+ ] = useLocalState(context, 'highscore', 0);
+ const highscore = highscores[highScoreIndex];
+ if (!highscore) {
+ return null;
+ }
+ const scores = Object
+ .keys(highscore.scores)
+ .map(key => ({
+ ckey: key,
+ value: highscore.scores[key],
+ }));
return (
-
- |
-
- |
-
- {name}
- {desc}
- 0 ? 'good' : 'bad'}>
- {value > 0 ? `Earned ${value} times` : 'Locked'}
-
- |
-
+
+
+ {highscores.map((highscore, i) => (
+ setHighScoreIndex(i)}>
+ {highscore.name}
+
+ ))}
+
+
+
+
+ #
+
+
+ Key
+
+
+ Score
+
+
+ {scores.map((score, i) => (
+
+
+ {i + 1}
+
+
+ {i === 0 && (
+
+ )}
+ {score.ckey}
+ {i === 0 && (
+
+ )}
+
+
+ {score.value}
+
+
+ ))}
+
+
);
};
diff --git a/tgui/packages/tgui/interfaces/Cargo.js b/tgui/packages/tgui/interfaces/Cargo.js
index fa29c30c11f..1b9771ad5a2 100644
--- a/tgui/packages/tgui/interfaces/Cargo.js
+++ b/tgui/packages/tgui/interfaces/Cargo.js
@@ -1,7 +1,8 @@
import { toArray } from 'common/collections';
import { Fragment } from 'inferno';
import { useBackend, useSharedState } from '../backend';
-import { AnimatedNumber, Box, Button, Divider, Flex, LabeledList, Section, Table } from '../components';
+import { AnimatedNumber, Box, Button, Flex, LabeledList, Section, Table, Tabs } from '../components';
+import { formatMoney } from '../format';
import { Window } from '../layouts';
export const Cargo = (props, context) => {
@@ -16,40 +17,34 @@ export const Cargo = (props, context) => {
-
-
-
+
{!requestonly && (
-
+
)}
-
+
{tab === 'catalog' && (
)}
@@ -81,7 +76,10 @@ const CargoStatus = (props, context) => {
title="Cargo"
buttons={(
- credits
+ formatMoney(value)} />
+ {' credits'}
)}>
@@ -143,19 +141,16 @@ export const CargoCatalog = (props, context) => {
)}>
- {supplies.map(supply => (
-
- ))}
-
-
-
+
+ {supplies.map(supply => (
+ setActiveSupplyName(supply.name)}>
+ {supply.name} ({supply.packs.length})
+
+ ))}
+
@@ -190,9 +185,9 @@ export const CargoCatalog = (props, context) => {
onClick={() => act('add', {
id: pack.id,
})}>
- {self_paid
+ {formatMoney(self_paid
? Math.round(pack.cost * 1.1)
- : pack.cost}
+ : pack.cost)}
{' cr'}
@@ -247,7 +242,7 @@ const CargoRequests = (props, context) => {
{request.reason}
- {request.cost} cr.
+ {formatMoney(request.cost)} cr
{!requestonly && (
@@ -290,7 +285,7 @@ const CargoCartButtons = (props, context) => {
{cart.length === 1 && '1 item'}
{cart.length >= 2 && cart.length + ' items'}
{' '}
- {total > 0 && `(${total} cr)`}
+ {total > 0 && `(${formatMoney(total)} cr)`}
- {entry.cost} cr.
+ {formatMoney(entry.cost)} cr