Files
CHOMPStation2/tgui/packages/tgui_ch/interfaces/EmbeddedController.js
2023-05-23 17:43:01 +02:00

662 lines
20 KiB
JavaScript

import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, Flex, Icon, LabeledList, ProgressBar, Section } from '../components';
import { Window } from '../layouts';
import { createLogger } from '../logging';
const logger = createLogger('fuck');
// This UI uses an internal routing system for the many different variants of
// embedded controllers in use.
let primaryRoutes = {};
/**
* This is an all-in-one replacement for the following NanoUI Templates:
* - advanced_airlock_console.tmpl
* - docking_airlock_console.tmpl
* - door_access_console.tmpl
* - escape_pod_console.tmpl
* - escape_pod_berth_console.tmpl
* - multi_docking_console.tmpl
* - phoron_airlock_console.tmpl
* - simple_airlock_console.tmpl
* - simple_docking_console.tmpl
* - simple_docking_console_pod.tmpl -- Funny enough, wasn't used anywhere.
*/
/**
* Let's cover all of the attributes of `data` for this UI right here.
* For those unfamiliar with JSDoc syntax, [param] indicates
* an optional parameter.
*/
/**
* Interior/Exterior Door Status
* @typedef {Object} doorStatus
* @property {('open'|'closed')} state
* @property {('locked'|'unlocked')} lock
*/
/**
* Dock Status
* @typedef {('undocked'|'undocking'|'docking'|'docked')} dockStatus
*/
/**
* All possible data attributes.
* @typedef {Object} Data
* @property {string} internalTemplateName - <Template> to use.
* @property {number} chamber_pressure - The current pressure of the airlock.
* @property {boolean} processing - Whether or not the airlock is currently
* cycling.
* @property {number} [external_pressure] - Pressure on the "external" side.
* @property {number} [internal_pressure] - Pressure on the "internal" side.
* @property {boolean} [purge] - Airlock currently purging?
* @property {boolean} [secure] - Airlock doors locked?
* @property {doorStatus} [exterior_status] - Describes the status of the
* exterior-side door.
* @property {doorStatus} [interior_status] - Describes the status of the
* interior-side door.
*
* @property {dockStatus} [docking_status] - Used exclusivly for "Docking" type
* controllers, describes the state of the dock.
* @property {boolean} [airlock_disabled] - Airlock disabled?
* @property {boolean} [override_enabled] - Forces the shuttle to undock.
* @property {string} [docking_codes] - The secret codes to dock a shuttle here
* @property {string} [name] - Name of the dock.
*/
/**
* Entrypoint of the UI. This handles finding the correct route to use.
*/
export const EmbeddedController = (props, context) => {
const { act, data } = useBackend(context);
const { internalTemplateName } = data;
const Component = primaryRoutes[internalTemplateName];
if (!Component) {
throw Error('Unable to find Component for template name: ' + internalTemplateName);
}
return (
<Window width={450} height={340} resizable>
<Window.Content>
<Component />
</Window.Content>
</Window>
);
};
/** ***************************************************************************\
* HELPER COMPONENTS *
\******************************************************************************/
/**
* @typedef {Object} BarProp
* @property {number} minValue - Minimum value of the bar.
* @property {number} maxValue - Maximum value of the bar.
* @property {number} value - Current value between min/max.
* @property {string} label - Label next to the bar.
* @property {string} textValue - Value in text.
*/
/**
* @typedef {Object} StatusDisplayProps
* @property {array[BarProp]} bars - The bars to display.
*/
/**
* Used for the upper status display that is used on 90% of these UIs.
* @param {StatusDisplayProps} props
*/
const StatusDisplay = (props, context) => {
const { bars } = props;
return (
<Section title="Status">
<LabeledList>
{bars.map((bar) => (
<LabeledList.Item key={bar.label} label={bar.label}>
<ProgressBar color={bar.color(bar.value)} minValue={bar.minValue} maxValue={bar.maxValue} value={bar.value}>
{bar.textValue}
</ProgressBar>
</LabeledList.Item>
))}
</LabeledList>
</Section>
);
};
/**
* This is just a quick helper for most airlock controllers. They usually all
* have the "Cycle out, cycle in, force out, force in" buttons, so we just have
* a single component that adjusts for the mild data structure differences
* on it's own.
*/
const StandardControls = (props, context) => {
const { data, act } = useBackend(context);
let externalForceSafe = true;
if (data['interior_status'] && data.interior_status.state === 'open') {
externalForceSafe = false;
} else if (data['external_pressure'] && data['chamber_pressure']) {
externalForceSafe = !(Math.abs(data['external_pressure'] - data['chamber_pressure']) > 5);
}
let internalForceSafe = true;
if (data['exterior_status'] && data.exterior_status.state === 'open') {
internalForceSafe = false;
} else if (data['internal_pressure'] && data['chamber_pressure']) {
internalForceSafe = !(Math.abs(data['internal_pressure'] - data['chamber_pressure']) > 5);
}
return (
<Fragment>
<Box>
<Button
disabled={data.airlock_disabled}
icon="arrow-left"
content="Cycle to Exterior"
onClick={() => act('cycle_ext')}
/>
<Button
disabled={data.airlock_disabled}
icon="arrow-right"
content="Cycle to Interior"
onClick={() => act('cycle_int')}
/>
</Box>
<Box>
<Button.Confirm
disabled={data.airlock_disabled}
color={externalForceSafe ? '' : 'bad'}
icon="exclamation-triangle"
confirmIcon="exclamation-triangle"
content="Force Exterior Door"
onClick={() => act('force_ext')}
/>
<Button.Confirm
disabled={data.airlock_disabled}
color={internalForceSafe ? '' : 'bad'}
icon="exclamation-triangle"
confirmIcon="exclamation-triangle"
content="Force Interior Door"
onClick={() => act('force_int')}
/>
</Box>
</Fragment>
);
};
/**
* This is a shared component between the EscapePodConsole
* and the EscapePodBerthConsole. They previously had different data structures
* but I got rid of that stupid shit.
*/
const EscapePodStatus = (props, context) => {
const { data, act } = useBackend(context);
const statusToHtml = {
'docked': <Armed />,
'undocking': <Box color="average">EJECTING-STAND CLEAR!</Box>,
'undocked': <Box color="grey">POD EJECTED</Box>,
'docking': <Box color="good">INITIALIZING...</Box>,
};
let dockHatch = <Box color="bad">ERROR</Box>;
if (data.exterior_status.state === 'open') {
dockHatch = <Box color="average">OPEN</Box>;
} else if (data.exterior_status.lock === 'unlocked') {
dockHatch = <Box color="average">UNSECURED</Box>;
} else if (data.exterior_status.lock === 'locked') {
dockHatch = <Box color="good">SECURED</Box>;
}
return (
<Section>
<LabeledList>
<LabeledList.Item label="Escape Pod Status">{statusToHtml[data.docking_status]}</LabeledList.Item>
<LabeledList.Item label="Docking Hatch">{dockHatch}</LabeledList.Item>
</LabeledList>
</Section>
);
};
/**
* Sub-subcomponent for escape pods.
* Just shows "ARMED" or "SYSTEMS OK" depending on armed status.
* Keeps me from having to write like, two lines of code.
*/
const Armed = (props, context) => {
const { data, act } = useBackend(context);
return data.armed ? <Box color="average">ARMED</Box> : <Box color="good">SYSTEMS OK</Box>;
};
/**
* Shared controls between the berth and the pod itself.
* Basically just external door control.
*/
const EscapePodControls = (props, context) => {
const { data, act } = useBackend(context);
return (
<Box>
<Button
disabled={!data.override_enabled}
icon="exclamation-triangle"
content="Force Exterior Door"
color={data.docking_status !== 'docked' ? 'bad' : ''}
onClick={() => act('force_door')}
/>
<Button
selected={data.override_enabled}
color={data.docking_status !== 'docked' ? 'bad' : 'average'}
icon="exclamation-triangle"
content="Override"
onClick={() => act('toggle_override')}
/>
</Box>
);
};
/**
* Just a neat little helper for all the different states of dock.
*/
const DockStatus = (props, context) => {
const { data, act } = useBackend(context);
const statusToHtml = {
'docked': <Box color="good">DOCKED</Box>,
'docking': <Box color="average">DOCKING</Box>,
'undocking': <Box color="average">UNDOCKING</Box>,
'undocked': <Box color="grey">NOT IN USE</Box>,
};
let dockStatus = statusToHtml[data.docking_status];
if (data.override_enabled) {
dockStatus = <Box color="bad">{data.docking_status.toUpperCase()}-OVERRIDE ENABLED</Box>;
}
return dockStatus;
};
/** ***************************************************************************\
* ROUTES *
\******************************************************************************/
/**
* Advanced airlock consoles display the external pressure,
* the internal pressure, and the chamber pressure separately.
* They also have a PURGE and SECURE option for safety.
* Replaces advanced_airlock_console.tmpl
*/
const AirlockConsoleAdvanced = (props, context) => {
const { act, data } = useBackend(context);
const color = (value) => {
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
};
const bars = [
{
minValue: 0,
maxValue: 202,
value: data.external_pressure,
label: 'External Pressure',
textValue: data.external_pressure + ' kPa',
color: color,
},
{
minValue: 0,
maxValue: 202,
value: data.chamber_pressure,
label: 'Chamber Pressure',
textValue: data.chamber_pressure + ' kPa',
color: color,
},
{
minValue: 0,
maxValue: 202,
value: data.internal_pressure,
label: 'Internal Pressure',
textValue: data.internal_pressure + ' kPa',
color: color,
},
];
return (
<Fragment>
<StatusDisplay bars={bars} />
<Section title="Controls">
<StandardControls />
<Box>
<Button icon="sync" content="Purge" onClick={() => act('purge')} />
<Button icon="lock-open" content="Secure" onClick={() => act('secure')} />
</Box>
<Box>
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
</Box>
</Section>
</Fragment>
);
};
primaryRoutes['AirlockConsoleAdvanced'] = AirlockConsoleAdvanced;
/**
* Simple airlock consoles are the least complicated airlock controller.
* They show the current chamber pressure, two cycle buttons, and two
* force door buttons. That's it.
* Replaces simple_airlock_console.tmpl
*/
const AirlockConsoleSimple = (props, context) => {
const { act, data } = useBackend(context);
const bars = [
{
minValue: 0,
maxValue: 202,
value: data.chamber_pressure,
label: 'Chamber Pressure',
textValue: data.chamber_pressure + ' kPa',
color: (value) => {
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
},
},
];
return (
<Fragment>
<StatusDisplay bars={bars} />
<Section title="Controls">
<StandardControls />
<Box>
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
</Box>
</Section>
</Fragment>
);
};
primaryRoutes['AirlockConsoleSimple'] = AirlockConsoleSimple;
/**
* Phoron airlock consoles don't actually cycle *pressure*, they cycle
* phoron, for use on transitioning to the outside environment of a phoron
* atmosphere planet.
* Replaces phoron_airlock_console.tmpl
*/
const AirlockConsolePhoron = (props, context) => {
const { act, data } = useBackend(context);
const bars = [
{
minValue: 0,
maxValue: 202,
value: data.chamber_pressure,
label: 'Chamber Pressure',
textValue: data.chamber_pressure + ' kPa',
color: (value) => {
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
},
},
{
minValue: 0,
maxValue: 100,
value: data.chamber_phoron,
label: 'Chamber Phoron',
textValue: data.chamber_phoron + ' mol',
color: (value) => {
return value > 5 ? 'bad' : value > 0.5 ? 'average' : 'good';
},
},
];
return (
<Fragment>
<StatusDisplay bars={bars} />
<Section title="Controls">
<StandardControls />
<Box>
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
</Box>
</Section>
</Fragment>
);
};
primaryRoutes['AirlockConsolePhoron'] = AirlockConsolePhoron;
/**
* This is a mix airlock & docking console. It lets you control the dock status
* as well as the attached airlock.
* Replaces docking_airlock_console.tmpl
*/
const AirlockConsoleDocking = (props, context) => {
const { act, data } = useBackend(context);
const bars = [
{
minValue: 0,
maxValue: 202,
value: data.chamber_pressure,
label: 'Chamber Pressure',
textValue: data.chamber_pressure + ' kPa',
color: (value) => {
return value < 80 || value > 120 ? 'bad' : value < 95 || value > 110 ? 'average' : 'good';
},
},
];
return (
<Fragment>
<Section
title="Dock"
buttons={
data.airlock_disabled || data.override_enabled ? (
<Button
icon="exclamation-triangle"
color={data.override_enabled ? 'red' : ''}
content="Override"
onClick={() => act('toggle_override')}
/>
) : null
}>
<DockStatus />
</Section>
<StatusDisplay bars={bars} />
<Section title="Controls">
<StandardControls />
<Box>
<Button disabled={!data.processing} icon="ban" color="bad" content="Abort" onClick={() => act('abort')} />
</Box>
</Section>
</Fragment>
);
};
primaryRoutes['AirlockConsoleDocking'] = AirlockConsoleDocking;
/**
* Simple docking consoles do not allow you to cycle the airlock. They can
* force the doors in an emergency, but there is no facility for cycling.
* They're primarily just there to display the status of the dock.
* Replaces simple_docking_console.tmpl
*/
const DockingConsoleSimple = (props, context) => {
const { act, data } = useBackend(context);
let dockHatch = <Box color="bad">ERROR</Box>;
if (data.exterior_status.state === 'open') {
dockHatch = <Box color="average">OPEN</Box>;
} else if (data.exterior_status.lock === 'unlocked') {
dockHatch = <Box color="average">UNSECURED</Box>;
} else if (data.exterior_status.lock === 'locked') {
dockHatch = <Box color="good">SECURED</Box>;
}
return (
<Section
title="Status"
buttons={
<Fragment>
<Button
icon="exclamation-triangle"
disabled={!data.override_enabled}
content="Force exterior door"
onClick={() => act('force_door')}
/>
<Button
icon="exclamation-triangle"
color={data.override_enabled ? 'red' : ''}
content="Override"
onClick={() => act('toggle_override')}
/>
</Fragment>
}>
<LabeledList>
<LabeledList.Item label="Dock Status">
<DockStatus />
</LabeledList.Item>
<LabeledList.Item label="Docking Hatch">{dockHatch}</LabeledList.Item>
</LabeledList>
</Section>
);
};
primaryRoutes['DockingConsoleSimple'] = DockingConsoleSimple;
/**
* Shockingly, the multi docking console is the simplest docking console.
* It has no functionality except to display the status of multiple airlocks,
* for bigger shuttles.
* Replaces multi_docking_console.tmpl
*/
const DockingConsoleMulti = (props, context) => {
const { data } = useBackend(context);
return (
<Fragment>
<Section title="Docking Status">
<DockStatus />
</Section>
<Section title="Airlocks">
{data.airlocks.length ? (
<LabeledList>
{data.airlocks.map((airlock) => (
<LabeledList.Item
color={airlock.override_enabled ? 'bad' : 'good'}
key={airlock.name}
label={airlock.name}>
{airlock.override_enabled ? 'OVERRIDE ENABLED' : 'STATUS OK'}
</LabeledList.Item>
))}
</LabeledList>
) : (
<Flex height="100%" mt="0.5em">
<Flex.Item grow="1" align="center" textAlign="center" color="bad">
<Icon name="door-closed" mb="0.5rem" size="5" />
<br />
No airlocks found.
</Flex.Item>
</Flex>
)}
</Section>
</Fragment>
);
};
primaryRoutes['DockingConsoleMulti'] = DockingConsoleMulti;
/**
* Airlock but without anything other than doors. Separates clean rooms.
* Replaces door_access_console.tmpl
*/
const DoorAccessConsole = (props, context) => {
const { act, data } = useBackend(context);
let interiorOpen = data.interior_status.state === 'open' || data.exterior_status.state === 'closed';
let exteriorOpen = data.exterior_status.state === 'open' || data.interior_status.state === 'closed';
return (
<Section
title="Status"
buttons={
<Fragment>
{/* Interior Button */}
<Button
icon={interiorOpen ? 'arrow-left' : 'exclamation-triangle'}
content={interiorOpen ? 'Cycle To Exterior' : 'Lock Exterior Door'}
onClick={() => {
act(interiorOpen ? 'cycle_ext_door' : 'force_ext');
}}
/>
{/* Exterior Button */}
<Button
icon={exteriorOpen ? 'arrow-right' : 'exclamation-triangle'}
content={exteriorOpen ? 'Cycle To Interior' : 'Lock Interior Door'}
onClick={() => {
act(exteriorOpen ? 'cycle_int_door' : 'force_int');
}}
/>
</Fragment>
}>
<LabeledList>
<LabeledList.Item label="Exterior Door Status">
{data.exterior_status.state === 'closed' ? 'Locked' : 'Open'}
</LabeledList.Item>
<LabeledList.Item label="Interior Door Status">
{data.interior_status.state === 'closed' ? 'Locked' : 'Open'}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
primaryRoutes['DoorAccessConsole'] = DoorAccessConsole;
/**
* These are the least airlock-like UIs here, but they're "close enough".
* Replaces escape_pod_console.tmpl
*/
const EscapePodConsole = (props, context) => {
const { act, data } = useBackend(context);
return (
<Fragment>
<EscapePodStatus />
<Section title="Controls">
<EscapePodControls />
<Box>
<Button
icon="exclamation-triangle"
disabled={data.armed}
color={data.armed ? 'bad' : 'average'}
content="ARM"
onClick={() => act('manual_arm')}
/>
<Button
icon="exclamation-triangle"
disabled={!data.can_force}
color="bad"
content="MANUAL EJECT"
onClick={() => act('force_launch')}
/>
</Box>
</Section>
</Fragment>
);
};
primaryRoutes['EscapePodConsole'] = EscapePodConsole;
/**
* These are the least airlock-like UIs here, but they're "close enough".
* Replaces escape_pod_berth_console.tmpl
*/
const EscapePodBerthConsole = (props, context) => {
const { data } = useBackend(context);
return (
<Fragment>
<EscapePodStatus />
<Section title="Controls">
<EscapePodControls />
</Section>
</Fragment>
);
};
primaryRoutes['EscapePodBerthConsole'] = EscapePodBerthConsole;