diff --git a/tgui/packages/tgui/interfaces/AbductorConsole.js b/tgui/packages/tgui/interfaces/AbductorConsole.js
new file mode 100644
index 0000000000..de750bc9ca
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AbductorConsole.js
@@ -0,0 +1,157 @@
+import { GenericUplink } from './Uplink';
+import { useBackend, useSharedState } from '../backend';
+import { Button, LabeledList, NoticeBox, Section, Tabs } from '../components';
+import { Fragment } from 'inferno';
+import { Window } from '../layouts';
+
+export const AbductorConsole = (props, context) => {
+ const [tab, setTab] = useSharedState(context, 'tab', 1);
+ return (
+
+
+
+ setTab(1)}>
+ Abductsoft 3000
+
+ setTab(2)}>
+ Mission Settings
+
+
+ {tab === 1 && (
+
+ )}
+ {tab === 2 && (
+
+
+
+
+ )}
+
+
+ );
+};
+
+const Abductsoft = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ experiment,
+ points,
+ credits,
+ } = data;
+
+ if (!experiment) {
+ return (
+
+ No Experiment Machine Detected
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+};
+
+const EmergencyTeleporter = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ pad,
+ gizmo,
+ } = data;
+
+ if (!pad) {
+ return (
+
+ No Telepad Detected
+
+ );
+ }
+
+ return (
+ act('teleporter_send')} />
+ )}>
+
+
+
+
+
+ );
+};
+
+const VestSettings = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ vest,
+ vest_mode,
+ vest_lock,
+ } = data;
+
+ if (!vest) {
+ return (
+
+ No Agent Vest Detected
+
+ );
+ }
+
+ return (
+ act('toggle_vest')} />
+ )}>
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ApcControl.js b/tgui/packages/tgui/interfaces/ApcControl.js
new file mode 100644
index 0000000000..449101d529
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ApcControl.js
@@ -0,0 +1,306 @@
+import { map, sortBy } from 'common/collections';
+import { flow } from 'common/fp';
+import { pureComponentHooks } from 'common/react';
+import { useBackend, useLocalState } from '../backend';
+import { Box, Button, Dimmer, Flex, Icon, Table, Tabs } from '../components';
+import { Fragment, Window } from '../layouts';
+import { AreaCharge, powerRank } from './PowerMonitor';
+
+export const ApcControl = (props, context) => {
+ const { data } = useBackend(context);
+ return (
+
+ {data.authenticated === 1 && (
+
+ )}
+ {data.authenticated === 0 && (
+
+ )}
+
+ );
+};
+
+const ApcLoggedOut = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { emagged } = data;
+ const text = emagged === 1 ? 'Open' : 'Log In';
+ return (
+
+
+ );
+};
+
+const ApcLoggedIn = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { restoring } = data;
+ const [
+ tabIndex,
+ setTabIndex,
+ ] = useLocalState(context, 'tab-index', 1);
+ return (
+
+
+ {
+ setTabIndex(1);
+ act('check-apcs');
+ }}>
+ APC Control Panel
+
+ {
+ setTabIndex(2);
+ act('check-logs');
+ }}>
+ Log View Panel
+
+
+ {restoring === 1 && (
+
+
+ {' Resetting...'}
+
+ )}
+ {tabIndex === 1 && (
+
+
+
+
+
+
+
+
+ )}
+ {tabIndex === 2 && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+const ControlPanel = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ emagged,
+ logging,
+ } = data;
+ const [
+ sortByField,
+ setSortByField,
+ ] = useLocalState(context, 'sortByField', null);
+ return (
+
+
+
+ Sort by:
+
+ setSortByField(sortByField !== 'name' && 'name')} />
+ setSortByField(
+ sortByField !== 'charge' && 'charge'
+ )} />
+ setSortByField(sortByField !== 'draw' && 'draw')} />
+
+
+
+ {emagged === 1 && (
+
+
+ )}
+ act('log-out')}
+ />
+
+
+ );
+};
+
+const ApcControlScene = (props, context) => {
+ const { data, act } = useBackend(context);
+
+ const [
+ sortByField,
+ ] = useLocalState(context, 'sortByField', null);
+
+ const apcs = flow([
+ map((apc, i) => ({
+ ...apc,
+ // Generate a unique id
+ id: apc.name + i,
+ })),
+ sortByField === 'name' && sortBy(apc => apc.name),
+ sortByField === 'charge' && sortBy(apc => -apc.charge),
+ sortByField === 'draw' && sortBy(
+ apc => -powerRank(apc.load),
+ apc => -parseFloat(apc.load)),
+ ])(data.apcs);
+ return (
+
+
+
+ On/Off
+
+
+ Area
+
+
+ Charge
+
+
+ Draw
+
+
+ Eqp
+
+
+ Lgt
+
+
+ Env
+
+
+ {apcs.map((apc, i) => (
+
+ |
+ act('breaker', {
+ ref: apc.ref,
+ })}
+ />
+ |
+
+ act('access-apc', {
+ ref: apc.ref,
+ })}>
+ {apc.name}
+
+ |
+
+
+ |
+
+ {apc.load}
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ ))}
+
+ );
+};
+
+const LogPanel = (props, context) => {
+ const { data } = useBackend(context);
+
+ const logs = flow([
+ map((line, i) => ({
+ ...line,
+ // Generate a unique id
+ id: line.entry + i,
+ })),
+ logs => logs.reverse(),
+ ])(data.logs);
+ return (
+
+ {logs.map(line => (
+
+ {line.entry}
+
+ ))}
+
+ );
+};
+
+const AreaStatusColorButton = props => {
+ const { target, status, apc, act } = props;
+ const power = Boolean(status & 2);
+ const mode = Boolean(status & 1);
+ return (
+ act('toggle-minor', {
+ type: target,
+ value: statusChange(status),
+ ref: apc.ref,
+ })}
+ />
+ );
+};
+
+const statusChange = status => {
+ // mode flip power flip both flip
+ // 0, 2, 3
+ return status === 0 ? 2 : status === 2 ? 3 : 0;
+};
+
+AreaStatusColorButton.defaultHooks = pureComponentHooks;
+
diff --git a/tgui/packages/tgui/interfaces/BluespaceLocator.js b/tgui/packages/tgui/interfaces/BluespaceLocator.js
new file mode 100644
index 0000000000..ef063a4942
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/BluespaceLocator.js
@@ -0,0 +1,101 @@
+import { useBackend, useSharedState } from '../backend';
+import { Icon, ProgressBar, Tabs } from '../components';
+import { Window } from '../layouts';
+
+const directionToIcon = {
+ north: 0,
+ northeast: 45,
+ east: 90,
+ southeast: 135,
+ south: 180,
+ southwest: 225,
+ west: 270,
+ northwest: 315,
+};
+
+export const BluespaceLocator = (props, context) => {
+ const [tab, setTab] = useSharedState(context, "tab", "implant");
+ return (
+
+
+
+ setTab("implant")}>
+ Implants
+
+ setTab("beacon")}>
+ Teleporter Beacons
+
+
+ {tab === "beacon" && (
+
+ )
+ || tab === "implant" && (
+
+ )}
+
+
+ );
+};
+
+const TeleporterBeacons = (props, context) => {
+ const { data } = useBackend(context);
+ const { telebeacons } = data;
+ return (
+ telebeacons.map(beacon => (
+
+ ))
+ );
+};
+
+const TrackingImplants = (props, context) => {
+ const { data } = useBackend(context);
+ const { trackimplants } = data;
+ return (
+ trackimplants.map(implant => (
+
+ ))
+ );
+};
+
+const SignalLocator = (props, context) => {
+ const { data } = useBackend(context);
+ const { trackingrange } = data;
+ const {
+ name,
+ direction,
+ distance,
+ } = props;
+ return (
+
+ {name}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/CargoBountyConsole.js b/tgui/packages/tgui/interfaces/CargoBountyConsole.js
new file mode 100644
index 0000000000..ad485231eb
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/CargoBountyConsole.js
@@ -0,0 +1,119 @@
+import { useBackend } from '../backend';
+import { AnimatedNumber, Box, Button, Section, Table } from '../components';
+import { formatMoney } from '../format';
+import { Window } from '../layouts';
+
+export const CargoBountyConsole = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ bountydata = [],
+ } = data;
+ return (
+
+
+ }
+ buttons={(
+ act('Print')} />
+ )}>
+
+
+
+ Bounty Object
+
+
+ Description
+
+
+ Progress
+
+
+ Value
+
+
+ Claim
+
+
+ {bountydata.map(bounty => (
+
+
+ {bounty.name}
+
+
+ {bounty.description}
+
+
+ {bounty.priority === 1
+ ? High Priority
+ : ""}
+ {bounty.completion_string}
+
+
+ {bounty.reward_string}
+
+
+ act('ClaimBounty', {
+ bounty: bounty.bounty_ref,
+ })} />
+
+
+ ))}
+
+
+
+
+ );
+};
+
+const BountyHeader = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ stored_cash,
+ } = data;
+ return (
+
+ formatMoney(value)} />
+ {' credits'}
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.js b/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.js
new file mode 100644
index 0000000000..5fe3874422
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/CivCargoHoldTerminal.js
@@ -0,0 +1,103 @@
+import { Fragment } from 'inferno';
+import { useBackend } from '../backend';
+import { AnimatedNumber, Box, Button, Flex, LabeledList, NoticeBox, Section } from '../components';
+import { Window } from '../layouts';
+
+export const CivCargoHoldTerminal = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ pad,
+ sending,
+ status_report,
+ id_inserted,
+ id_bounty_info,
+ id_bounty_value,
+ id_bounty_num,
+ } = data;
+ const in_text = "Welcome valued employee.";
+ const out_text = "To begin, insert your ID into the console.";
+ return (
+
+
+
+
+
+ {id_inserted ? in_text : out_text}
+
+
+
+
+ {pad ? "Online" : "Not Found"}
+
+
+ {status_report}
+
+
+
+
+
+
+
+ act('recalc')} />
+ act(sending ? 'stop' : 'send')} />
+ act('bounty')} />
+ act('eject')} />
+
+
+
+
+
+ );
+};
+
+const BountyTextBox = (props, context) => {
+ const { data } = useBackend(context);
+ const {
+ id_bounty_info,
+ id_bounty_value,
+ id_bounty_num,
+ } = data;
+ const na_text = "N/A, please add a new bounty.";
+ return (
+
+
+
+ {id_bounty_info ? id_bounty_info : na_text}
+
+
+ {id_bounty_info ? id_bounty_num : "N/A"}
+
+
+ {id_bounty_info ? id_bounty_value : "N/A"}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ExosuitFabricator.js b/tgui/packages/tgui/interfaces/ExosuitFabricator.js
new file mode 100644
index 0000000000..a9c42d91a8
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ExosuitFabricator.js
@@ -0,0 +1,796 @@
+import { classes } from 'common/react';
+import { uniqBy } from 'common/collections';
+import { useBackend, useSharedState } from '../backend';
+import { formatSiUnit, formatMoney } from '../format';
+import { Flex, Section, Tabs, Box, Button, Fragment, ProgressBar, NumberInput, Icon, Input } from '../components';
+import { Window } from '../layouts';
+import { createSearch } from 'common/string';
+
+const MATERIAL_KEYS = {
+ "iron": "sheet-metal_3",
+ "glass": "sheet-glass_3",
+ "silver": "sheet-silver_3",
+ "gold": "sheet-gold_3",
+ "diamond": "sheet-diamond",
+ "plasma": "sheet-plasma_3",
+ "uranium": "sheet-uranium",
+ "bananium": "sheet-bananium",
+ "titanium": "sheet-titanium_3",
+ "bluespace crystal": "polycrystal",
+ "plastic": "sheet-plastic_3",
+};
+
+const COLOR_NONE = 0;
+const COLOR_AVERAGE = 1;
+const COLOR_BAD = 2;
+
+const COLOR_KEYS = {
+ [COLOR_NONE]: false,
+ [COLOR_AVERAGE]: "average",
+ [COLOR_BAD]: "bad",
+};
+
+const materialArrayToObj = materials => {
+ let materialObj = {};
+
+ materials.forEach(m => {
+ materialObj[m.name] = m.amount; });
+
+ return materialObj;
+};
+
+const partBuildColor = (cost, tally, material) => {
+ if (cost > material) {
+ return { color: COLOR_BAD, deficit: (cost - material) };
+ }
+
+ if (tally > material) {
+ return { color: COLOR_AVERAGE, deficit: cost };
+ }
+
+ if (cost + tally > material) {
+ return { color: COLOR_AVERAGE, deficit: ((cost + tally) - material) };
+ }
+
+ return { color: COLOR_NONE, deficit: 0 };
+};
+
+const partCondFormat = (materials, tally, part) => {
+ let format = { "textColor": COLOR_NONE };
+
+ Object.keys(part.cost).forEach(mat => {
+ format[mat] = partBuildColor(part.cost[mat], tally[mat], materials[mat]);
+
+ if (format[mat].color > format["textColor"]) {
+ format["textColor"] = format[mat].color;
+ }
+ });
+
+ return format;
+};
+
+const queueCondFormat = (materials, queue) => {
+ let materialTally = {};
+ let matFormat = {};
+ let missingMatTally = {};
+ let textColors = {};
+
+ queue.forEach((part, i) => {
+ textColors[i] = COLOR_NONE;
+ Object.keys(part.cost).forEach(mat => {
+ materialTally[mat] = materialTally[mat] || 0;
+ missingMatTally[mat] = missingMatTally[mat] || 0;
+
+ matFormat[mat] = partBuildColor(
+ part.cost[mat], materialTally[mat], materials[mat]
+ );
+
+ if (matFormat[mat].color !== COLOR_NONE) {
+ if (textColors[i] < matFormat[mat].color) {
+ textColors[i] = matFormat[mat].color;
+ }
+ }
+ else {
+ materialTally[mat] += part.cost[mat];
+ }
+
+ missingMatTally[mat] += matFormat[mat].deficit;
+ });
+ });
+ return { materialTally, missingMatTally, textColors, matFormat };
+};
+
+const searchFilter = (search, allparts) => {
+ let searchResults = [];
+
+ if (!search.length) {
+ return;
+ }
+
+ const resultFilter = createSearch(search, part => (
+ (part.name || "")
+ + (part.desc || "")
+ + (part.searchMeta || "")
+ ));
+
+ Object.keys(allparts).forEach(category => {
+ allparts[category]
+ .filter(resultFilter)
+ .forEach(e => { searchResults.push(e); });
+ });
+
+ searchResults = uniqBy(part => part.name)(searchResults);
+
+ return searchResults;
+};
+
+export const ExosuitFabricator = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const queue = data.queue || [];
+ const materialAsObj = materialArrayToObj(data.materials || []);
+
+ const {
+ materialTally,
+ missingMatTally,
+ textColors,
+ } = queueCondFormat(materialAsObj, queue);
+
+ const [
+ displayMatCost,
+ setDisplayMatCost,
+ ] = useSharedState(context, "display_mats", false);
+
+ return (
+
+
+
+
+
+
+
+
+
+ setDisplayMatCost(!displayMatCost)}
+ checked={displayMatCost}>
+ Display Material Costs
+
+
+
+
+
+
+
+ act("sync_rnd")} />
+ )}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const EjectMaterial = (props, context) => {
+ const { act } = useBackend(context);
+
+ const { material } = props;
+
+ const {
+ name,
+ removable,
+ sheets,
+ ref,
+ } = material;
+
+ const [
+ removeMaterials,
+ setRemoveMaterials,
+ ] = useSharedState(context, "remove_mats_" + name, 1);
+
+ if ((removeMaterials > 1) && (sheets < removeMaterials)) {
+ setRemoveMaterials(sheets || 1);
+ }
+
+ return (
+
+ {
+ const newVal = parseInt(val, 10);
+ if (Number.isInteger(newVal)) {
+ setRemoveMaterials(newVal);
+ }
+ }} />
+ act("remove_mat", {
+ ref: ref,
+ amount: removeMaterials,
+ })} />
+
+ );
+};
+
+const Materials = (props, context) => {
+ const { data } = useBackend(context);
+
+ const materials = data.materials || [];
+
+ return (
+
+ {materials.map(material => (
+
+
+
+
+
+
+ ))}
+
+ );
+};
+
+const MaterialAmount = (props, context) => {
+ const {
+ name,
+ amount,
+ formatsi,
+ formatmoney,
+ color,
+ style,
+ } = props;
+
+ return (
+
+
+
+
+
+
+ {(formatsi && formatSiUnit(amount, 0))
+ || (formatmoney && formatMoney(amount))
+ || (amount)}
+
+
+
+ );
+};
+
+const PartSets = (props, context) => {
+ const { data } = useBackend(context);
+
+ const partSets = data.partSets || [];
+ const buildableParts = data.buildableParts || {};
+
+ const [
+ selectedPartTab,
+ setSelectedPartTab,
+ ] = useSharedState(
+ context,
+ "part_tab",
+ partSets.length ? buildableParts[0] : ""
+ );
+
+ return (
+
+ {partSets.map(set => (
+ !!(buildableParts[set]) && (
+ setSelectedPartTab(set)}>
+ {set}
+
+ )
+ ))}
+
+ );
+};
+
+const PartLists = (props, context) => {
+ const { data } = useBackend(context);
+
+ const getFirstValidPartSet = (sets => {
+ for (let set of sets) {
+ if (buildableParts[set]) {
+ return set;
+ }
+ }
+ return null;
+ });
+
+ const partSets = data.partSets || [];
+ const buildableParts = data.buildableParts || [];
+
+ const {
+ queueMaterials,
+ materials,
+ } = props;
+
+ const [
+ selectedPartTab,
+ setSelectedPartTab,
+ ] = useSharedState(
+ context,
+ "part_tab",
+ getFirstValidPartSet(partSets)
+ );
+
+ const [
+ searchText,
+ setSearchText,
+ ] = useSharedState(context, "search_text", "");
+
+ if (!selectedPartTab || !buildableParts[selectedPartTab]) {
+ const validSet = getFirstValidPartSet(partSets);
+ if (validSet) {
+ setSelectedPartTab(validSet);
+ }
+ else {
+ return;
+ }
+ }
+
+ let partsList;
+ // Build list of sub-categories if not using a search filter.
+ if (!searchText) {
+ partsList = { "Parts": [] };
+ buildableParts[selectedPartTab].forEach(part => {
+ part["format"] = partCondFormat(materials, queueMaterials, part);
+ if (!part.subCategory) {
+ partsList["Parts"].push(part);
+ return;
+ }
+ if (!(part.subCategory in partsList)) {
+ partsList[part.subCategory] = [];
+ }
+ partsList[part.subCategory].push(part);
+ });
+ }
+ else {
+ partsList = [];
+ searchFilter(searchText, buildableParts).forEach(part => {
+ part["format"] = partCondFormat(materials, queueMaterials, part);
+ partsList.push(part);
+ });
+ }
+
+
+ return (
+
+
+ {(!!searchText && (
+
+ )) || (
+ Object.keys(partsList).map(category => (
+
+ ))
+ )}
+
+ );
+};
+
+const PartCategory = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const {
+ buildingPart,
+ } = data;
+
+ const {
+ parts,
+ name,
+ forceShow,
+ placeholder,
+ } = props;
+
+ const [
+ displayMatCost,
+ ] = useSharedState(context, "display_mats", false);
+
+ return (
+ ((!!parts.length || forceShow) && (
+ act("add_queue_set", {
+ part_list: parts.map(part => part.id),
+ })} />
+ }>
+ {(!parts.length) && (placeholder)}
+ {parts.map(part => (
+
+
+
+ act("build_part", { id: part.id })} />
+
+
+ act("add_queue_part", { id: part.id })} />
+
+
+
+ {part.name}
+
+
+
+
+
+
+
+ {(displayMatCost && (
+
+ {Object.keys(part.cost).map(material => (
+
+
+
+ ))}
+
+ ))}
+
+
+ ))}
+
+ ))
+ );
+};
+
+const Queue = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const { isProcessingQueue } = data;
+
+ const queue = data.queue || [];
+
+ const {
+ queueMaterials,
+ missingMaterials,
+ textColors,
+ } = props;
+
+ return (
+
+
+
+ act("clear_queue")} />
+ {(!!isProcessingQueue && (
+ act("stop_queue")} />
+ )) || (
+ act("build_queue")} />
+ )}
+
+ }>
+
+
+
+
+
+
+
+
+
+
+ {!!queue.length && (
+
+
+
+ )}
+
+ );
+};
+
+const QueueMaterials = (props, context) => {
+ const {
+ queueMaterials,
+ missingMaterials,
+ } = props;
+
+ return (
+
+ {Object.keys(queueMaterials).map(material => (
+
+
+ {(!!missingMaterials[material] && (
+
+ {formatMoney(missingMaterials[material])}
+
+ ))}
+
+ ))}
+
+ );
+};
+
+const QueueList = (props, context) => {
+ const { act, data } = useBackend(context);
+
+ const {
+ textColors,
+ } = props;
+
+ const queue = data.queue || [];
+
+ if (!queue.length) {
+ return (
+
+ No parts in queue.
+
+ );
+ }
+
+ return (
+ queue.map((part, index) => (
+
+
+
+ act("del_queue_part", { index: index+1 })} />
+
+
+
+ {part.name}
+
+
+
+
+ ))
+ );
+};
+
+const BeingBuilt = (props, context) => {
+ const { data } = useBackend(context);
+
+ const {
+ buildingPart,
+ storedPart,
+ } = data;
+
+ if (storedPart) {
+ const {
+ name,
+ } = storedPart;
+
+ return (
+
+
+
+
+ {name}
+
+
+
+ {"Fabricator outlet obstructed..."}
+
+
+
+
+ );
+ }
+
+ if (buildingPart) {
+ const {
+ name,
+ duration,
+ printTime,
+ } = buildingPart;
+
+ const timeLeft = Math.ceil(duration/10);
+
+ return (
+
+
+
+
+ {name}
+
+
+
+ {((timeLeft >= 0) && (timeLeft + "s")) || ("Dispensing...")}
+
+
+
+
+ );
+ }
+};
diff --git a/tgui/packages/tgui/interfaces/ForbiddenLore.js b/tgui/packages/tgui/interfaces/ForbiddenLore.js
new file mode 100644
index 0000000000..1225132810
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ForbiddenLore.js
@@ -0,0 +1,61 @@
+import { sortBy } from 'common/collections';
+import { flow } from 'common/fp';
+import { useBackend } from '../backend';
+import { Box, Button, Section } from '../components';
+import { Window } from '../layouts';
+
+export const ForbiddenLore = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ charges,
+ } = data;
+ const to_know = flow([
+ sortBy(to_know => to_know.state !== "Research",
+ to_know => to_know.path === "Side"),
+ ])(data.to_know || []);
+ return (
+
+
+
+ Charges left : {charges}
+ {to_know!== null ? (
+ to_know.map(knowledge => (
+
+
+ {knowledge.path} path
+
+
+ act('research', {
+ name: knowledge.name,
+ cost: knowledge.cost,
+ })} />
+ {' '}
+ Cost : {knowledge.cost}
+
+
+ {knowledge.flavour}
+
+
+ {knowledge.desc}
+
+
+ ))
+ ) : (
+
+ No more knowledge can be found
+
+ )}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/GlandDispenser.js b/tgui/packages/tgui/interfaces/GlandDispenser.js
new file mode 100644
index 0000000000..f572a92f06
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/GlandDispenser.js
@@ -0,0 +1,37 @@
+import { useBackend } from '../backend';
+import { Button, Section } from '../components';
+import { Window } from '../layouts';
+
+export const GlandDispenser = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ glands = [],
+ } = data;
+ return (
+
+
+
+ {glands.map(gland => (
+ act('dispense', {
+ gland_id: gland.id,
+ })} />
+ ))}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/Holopad.js b/tgui/packages/tgui/interfaces/Holopad.js
new file mode 100644
index 0000000000..4066544bd0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/Holopad.js
@@ -0,0 +1,159 @@
+import { Fragment } from 'inferno';
+import { useBackend } from '../backend';
+import { Box, Button, Flex, Icon, LabeledList, Modal, NoticeBox, Section } from '../components';
+import { Window } from '../layouts';
+
+export const Holopad = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ calling,
+ } = data;
+ return (
+
+ {!!calling && (
+
+
+
+
+
+
+ {'Dialing...'}
+
+
+
+ act('hang_up')} />
+
+
+ )}
+
+
+
+
+ );
+};
+
+const HolopadContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ on_network,
+ on_cooldown,
+ allowed,
+ disk,
+ disk_record,
+ replay_mode,
+ loop_mode,
+ record_mode,
+ holo_calls = [],
+ } = data;
+ return (
+
+ act('AIrequest')} />
+ )} >
+
+
+ act('holocall', { headcall: allowed })} />
+
+ {holo_calls.map((call => {
+ return (
+
+ act(call.connected
+ ? 'disconnectcall'
+ : 'connectcall', { holopad: call.ref })} />
+
+ );
+ }))}
+
+
+ act('disk_eject')} />
+ }>
+ {!disk && (
+
+ No holodisk
+
+ ) || (
+
+
+ act('replay_mode')} />
+ act('loop_mode')} />
+ act('offset')} />
+
+
+ act('record_mode')} />
+ act('record_clear')} />
+
+
+ )}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/Jukebox.js b/tgui/packages/tgui/interfaces/Jukebox.js
new file mode 100644
index 0000000000..b23e55469b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/Jukebox.js
@@ -0,0 +1,109 @@
+import { sortBy } from 'common/collections';
+import { flow } from 'common/fp';
+import { useBackend } from '../backend';
+import { Box, Button, Dropdown, Section, Knob, LabeledControls, LabeledList } from '../components';
+import { Window } from '../layouts';
+
+export const Jukebox = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ active,
+ track_selected,
+ track_length,
+ track_beat,
+ volume,
+ } = data;
+ const songs = flow([
+ sortBy(
+ song => song.name),
+ ])(data.songs || []);
+ return (
+
+
+ act('toggle')} />
+ )}>
+
+
+ song.name)}
+ disabled={active}
+ selected={track_selected || "Select a Track"}
+ onSelected={value => act('select_track', {
+ track: value,
+ })} />
+
+
+ {track_selected ? track_length : "No Track Selected"}
+
+
+ {track_selected ? track_beat : "No Track Selected"}
+ {track_beat === 1 ? " beat" : " beats"}
+
+
+
+
+
+
+
+ = 50 ? 'red' : 'green'}
+ value={volume}
+ unit="%"
+ minValue={0}
+ maxValue={100}
+ step={1}
+ stepPixelSize={1}
+ disabled={active}
+ onDrag={(e, value) => act('set_volume', {
+ volume: value,
+ })} />
+ act('set_volume', {
+ volume: "min",
+ })} />
+ act('set_volume', {
+ volume: "max",
+ })} />
+ act('set_volume', {
+ volume: "reset",
+ })} />
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/NtosRequestKiosk.js b/tgui/packages/tgui/interfaces/NtosRequestKiosk.js
new file mode 100644
index 0000000000..63a8dc63f6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/NtosRequestKiosk.js
@@ -0,0 +1,15 @@
+import { RequestKioskContent } from './RequestKiosk';
+import { NtosWindow } from '../layouts';
+
+export const NtosRequestKiosk = (props, context) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PortableChemMixer.js b/tgui/packages/tgui/interfaces/PortableChemMixer.js
new file mode 100644
index 0000000000..0cef29e5a3
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PortableChemMixer.js
@@ -0,0 +1,114 @@
+import { toTitleCase } from 'common/string';
+import { Fragment } from 'inferno';
+import { useBackend } from '../backend';
+import { AnimatedNumber, Box, Button, LabeledList, Section } from '../components';
+import { Window } from '../layouts';
+import { sortBy } from 'common/collections';
+
+export const PortableChemMixer = (props, context) => {
+ const { act, data } = useBackend(context);
+ const recording = !!data.recordingRecipe;
+ const beakerTransferAmounts = data.beakerTransferAmounts || [];
+ const beakerContents = recording
+ && Object.keys(data.recordingRecipe)
+ .map(id => ({
+ id,
+ name: toTitleCase(id.replace(/_/, ' ')),
+ volume: data.recordingRecipe[id],
+ }))
+ || data.beakerContents
+ || [];
+ const chemicals = sortBy(chem => chem.title)(data.chemicals);
+ return (
+
+
+ (
+ act('amount', {
+ target: amount,
+ })} />
+ ))
+ )}>
+
+ {chemicals.map(chemical => (
+ act('dispense', {
+ reagent: chemical.id,
+ })} />
+ ))}
+
+
+ (
+ act('remove', { amount })} />
+ ))
+ )}>
+
+ act('eject')} />
+ )}>
+ {recording
+ && 'Virtual beaker'
+ || data.isBeakerLoaded
+ && (
+
+
+ /{data.beakerMaxVolume} units
+
+ )
+ || 'No beaker'}
+
+
+
+ {(!data.isBeakerLoaded && !recording) && 'N/A'
+ || beakerContents.length === 0 && 'Nothing'}
+
+ {beakerContents.map(chemical => (
+
+
+ {' '}
+ units of {chemical.name}
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/PortableTurret.js b/tgui/packages/tgui/interfaces/PortableTurret.js
new file mode 100644
index 0000000000..bb6700743c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PortableTurret.js
@@ -0,0 +1,108 @@
+import { Fragment } from 'inferno';
+import { useBackend } from '../backend';
+import { Button, LabeledList, NoticeBox, Section } from '../components';
+import { Window } from '../layouts';
+
+export const PortableTurret = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ silicon_user,
+ locked,
+ on,
+ check_weapons,
+ neutralize_criminals,
+ neutralize_all,
+ neutralize_unidentified,
+ neutralize_nonmindshielded,
+ neutralize_cyborgs,
+ ignore_heads,
+ manual_control,
+ allow_manual_control,
+ lasertag_turret,
+ } = data;
+ return (
+
+
+
+ Swipe an ID card to {locked ? 'unlock' : 'lock'} this interface.
+
+
+
+
+ act('manual')} />
+ )}>
+ act('power')} />
+
+
+
+ {!lasertag_turret && (
+ act('shootheads')} />
+ )}>
+ act('shootall')} />
+ act('authweapon')} />
+ act('checkxenos')} />
+ act('checkloyal')} />
+ act('shootcriminals')} />
+ act('shootborgs')} />
+
+ )}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/ProbingConsole.js b/tgui/packages/tgui/interfaces/ProbingConsole.js
new file mode 100644
index 0000000000..4115be4e4b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ProbingConsole.js
@@ -0,0 +1,83 @@
+import { useBackend } from '../backend';
+import { Button, LabeledList, NoticeBox, Section } from '../components';
+import { Window } from '../layouts';
+
+export const ProbingConsole = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ open,
+ feedback,
+ occupant,
+ occupant_name,
+ occupant_status,
+ } = data;
+ return (
+
+
+
+ act('door')} />
+ )}>
+ {occupant && (
+
+
+ {occupant_name}
+
+
+ {occupant_status === 3
+ ? 'Deceased'
+ : occupant_status === 2
+ ? 'Unconcious'
+ : 'Concious'}
+
+
+ act('experiment', {
+ experiment_type: 1,
+ })} />
+ act('experiment', {
+ experiment_type: 2,
+ })} />
+ act('experiment', {
+ experiment_type: 3,
+ })} />
+
+
+ ) || (
+
+ No Subject
+
+ )}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/RequestKiosk.js b/tgui/packages/tgui/interfaces/RequestKiosk.js
new file mode 100644
index 0000000000..4d2a563742
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/RequestKiosk.js
@@ -0,0 +1,155 @@
+import { Fragment } from 'inferno';
+import { useBackend } from '../backend';
+import { Box, Button, Collapsible, Flex, LabeledList, NumberInput, Section, TextArea } from '../components';
+import { formatMoney } from '../format';
+import { Window } from '../layouts';
+
+export const RequestKiosk = (props, context) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export const RequestKioskContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ accountName,
+ requests = [],
+ applicants = [],
+ bountyValue,
+ bountyText,
+ } = data;
+ const color = 'rgba(13, 13, 213, 0.7)';
+ const backColor = 'rgba(0, 0, 69, 0.5)';
+ return (
+
+
+
+ act('clear')} />
+ )}>
+ {accountName || 'N/A'}
+
+
+
+
+
+ {requests?.map(request => (
+
+
+
+
+ {request.owner}
+
+
+ {formatMoney(request.value) + ' cr'}
+
+
+ act('apply', {
+ request: request.acc_number,
+ })} />
+ act('deleteRequest', {
+ request: request.acc_number,
+ })} />
+
+
+
+ "{request.description}"
+
+
+ {applicants?.map(applicant => (
+ applicant.request_id === request.acc_number && (
+
+
+ {applicant.name}
+
+
+ act('payApplicant', {
+ applicant: applicant.requestee_id,
+ request: request.acc_number,
+ })} />
+
+
+ )
+ ))}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/Sleeper.js b/tgui/packages/tgui/interfaces/Sleeper.js
index de039d1fde..f175e65800 100644
--- a/tgui/packages/tgui/interfaces/Sleeper.js
+++ b/tgui/packages/tgui/interfaces/Sleeper.js
@@ -1,17 +1,34 @@
import { useBackend } from '../backend';
-import { Box, Section, LabeledList, Button, ProgressBar, AnimatedNumber } from '../components';
+import { Box, Section, LabeledList, Button, ProgressBar } from '../components';
import { Fragment } from 'inferno';
import { Window } from '../layouts';
+const damageTypes = [
+ {
+ label: 'Brute',
+ type: 'bruteLoss',
+ },
+ {
+ label: 'Burn',
+ type: 'fireLoss',
+ },
+ {
+ label: 'Toxin',
+ type: 'toxLoss',
+ },
+ {
+ label: 'Oxygen',
+ type: 'oxyLoss',
+ },
+];
+
export const Sleeper = (props, context) => {
const { act, data } = useBackend(context);
-
const {
open,
occupant = {},
occupied,
} = data;
-
const preSortChems = data.chems || [];
const chems = preSortChems.sort((a, b) => {
const descA = a.name.toLowerCase();
@@ -36,28 +53,10 @@ export const Sleeper = (props, context) => {
}
return 0;
});
-
- const damageTypes = [
- {
- label: 'Brute',
- type: 'bruteLoss',
- },
- {
- label: 'Burn',
- type: 'fireLoss',
- },
- {
- label: 'Toxin',
- type: 'toxLoss',
- },
- {
- label: 'Oxygen',
- type: 'oxyLoss',
- },
- ];
-
return (
-
+
{
key={chem.name}
icon="flask"
content={chem.name}
- disabled={!(occupied && chem.allowed)}
+ disabled={!occupied || !chem.allowed}
width="140px"
onClick={() => act('inject', {
chem: chem.id,
- })}
- />
+ })} />
))}
{
+ const { act, data } = useBackend(context);
+ const {
+ records = [],
+ } = data;
+ return (
+
+
+ {!records.length ? (
+
+ No Records
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+export const TachyonArrayContent = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ records = [],
+ } = data;
+ const [
+ activeRecordName,
+ setActiveRecordName,
+ ] = useSharedState(context, 'record', records[0]?.name);
+ const activeRecord = records.find(record => {
+ return record.name === activeRecordName;
+ });
+ return (
+
+
+
+
+ {records.map(record => (
+ setActiveRecordName(record.name)}>
+ {record.name}
+
+ ))}
+
+
+ {activeRecord ? (
+
+
+ act('delete_record', {
+ 'ref': activeRecord.ref,
+ })} />
+ act('print_record', {
+ 'ref': activeRecord.ref,
+ })} />
+
+ )}>
+
+
+ {activeRecord.timestamp}
+
+
+ {activeRecord.coordinates}
+
+
+ {activeRecord.displacement} seconds
+
+
+ {activeRecord.factual_epicenter_radius}
+ {activeRecord.theory_epicenter_radius
+ && " (Theoretical: "
+ + activeRecord.theory_epicenter_radius + ")"}
+
+
+ {activeRecord.factual_outer_radius}
+ {activeRecord.theory_outer_radius
+ && " (Theoretical: "
+ + activeRecord.theory_outer_radius + ")"}
+
+
+ {activeRecord.factual_shockwave_radius}
+ {activeRecord.theory_shockwave_radius
+ && " (Theoretical: "
+ + activeRecord.theory_shockwave_radius + ")"}
+
+
+
+
+ ) : (
+
+
+ No Record Selected
+
+
+ )}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/TurretControl.js b/tgui/packages/tgui/interfaces/TurretControl.js
new file mode 100644
index 0000000000..bbda8dd1fd
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/TurretControl.js
@@ -0,0 +1,51 @@
+import { useBackend } from '../backend';
+import { Button, LabeledList, Section } from '../components';
+import { Window } from '../layouts';
+import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
+
+export const TurretControl = (props, context) => {
+ const { act, data } = useBackend(context);
+ const locked = data.locked && !data.siliconUser;
+ const {
+ enabled,
+ lethal,
+ shootCyborgs,
+ } = data;
+ return (
+
+
+
+
+
+
+ act('power')} />
+
+
+ act('mode')} />
+
+
+ act('shoot_silicons')} />
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/Vendatray.js b/tgui/packages/tgui/interfaces/Vendatray.js
new file mode 100644
index 0000000000..55267be200
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/Vendatray.js
@@ -0,0 +1,97 @@
+import { Fragment } from 'inferno';
+import { useBackend } from '../backend';
+import { Box, Button, Flex, Section } from '../components';
+import { Window } from '../layouts';
+
+export const Vendatray = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ product_name,
+ product_cost,
+ tray_open,
+ registered,
+ owner_name,
+ } = data;
+ return (
+
+
+
+
+ {!!product_name && (
+
+ )}
+
+
+
+ {product_name ? product_name : "Empty"}
+
+ {product_name ? product_cost : "N/A"} cr
+ act('Adjust')} />
+
+
+
+ act('Open')} />
+ act('Buy')} />
+
+
+
+ {registered?(
+
+ Pays to the account of {owner_name}.
+
+ ):(
+
+
+ Tray is unregistered.
+
+ act('Register')} />
+
+ )}
+
+
+ );
+};
+
+const VendingImage = (props, context) => {
+ const { data } = useBackend(context);
+ const {
+ product_icon,
+ } = data;
+ return (
+
+ );
+};