mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-22 11:38:12 +01:00
TEG & Disposal Bin UI (#17602)
* Add pretty spinners to the TEG * Overhaul Disposal Bin UI
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
LabeledList,
|
||||
ProgressBar,
|
||||
Section,
|
||||
} from 'tgui-core/components';
|
||||
import { Box, Icon, RoundGauge, Section, Stack } from 'tgui-core/components';
|
||||
import type { BooleanLike } from 'tgui-core/react';
|
||||
|
||||
type Data = {
|
||||
@@ -20,97 +15,344 @@ type Data = {
|
||||
export const DisposalBin = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { mode, pressure, isAI, panel_open, flushing } = data;
|
||||
let stateColor;
|
||||
let stateText;
|
||||
if (mode === 2) {
|
||||
stateColor = 'good';
|
||||
stateText = 'Ready';
|
||||
} else if (mode <= 0) {
|
||||
stateColor = 'bad';
|
||||
stateText = 'N/A';
|
||||
} else if (mode === 1) {
|
||||
stateColor = 'average';
|
||||
stateText = 'Pressurizing';
|
||||
} else {
|
||||
stateColor = 'average';
|
||||
stateText = 'Idle';
|
||||
}
|
||||
|
||||
return (
|
||||
<Window width={300} height={250}>
|
||||
<Window width={190} height={260}>
|
||||
<Window.Content>
|
||||
<Section>
|
||||
<Box bold m={1}>
|
||||
Status
|
||||
</Box>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="State" color={stateColor}>
|
||||
{stateText}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Pressure">
|
||||
<ProgressBar
|
||||
ranges={{
|
||||
bad: [-Infinity, 0],
|
||||
average: [0, 99],
|
||||
good: [99, Infinity],
|
||||
}}
|
||||
value={pressure}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
<Box bold m={1}>
|
||||
Controls
|
||||
</Box>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Handle">
|
||||
<Button
|
||||
icon="toggle-off"
|
||||
disabled={isAI || panel_open}
|
||||
selected={flushing ? null : true}
|
||||
onClick={() => act('disengageHandle')}
|
||||
>
|
||||
Disengaged
|
||||
</Button>
|
||||
<Button
|
||||
icon="toggle-on"
|
||||
disabled={isAI || panel_open}
|
||||
selected={flushing ? true : null}
|
||||
onClick={() => act('engageHandle')}
|
||||
>
|
||||
Engaged
|
||||
</Button>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Power">
|
||||
<Button
|
||||
icon="toggle-off"
|
||||
disabled={mode === -1}
|
||||
selected={mode ? null : true}
|
||||
onClick={() => act('pumpOff')}
|
||||
>
|
||||
Off
|
||||
</Button>
|
||||
<Button
|
||||
icon="toggle-on"
|
||||
disabled={mode === -1}
|
||||
selected={mode ? true : null}
|
||||
onClick={() => act('pumpOn')}
|
||||
>
|
||||
On
|
||||
</Button>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Eject">
|
||||
<Button
|
||||
icon="sign-out-alt"
|
||||
disabled={isAI}
|
||||
onClick={() => act('eject')}
|
||||
>
|
||||
Eject Contents
|
||||
</Button>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
<Section fill>
|
||||
<Stack justify="space-around">
|
||||
<Stack.Item>
|
||||
<Stack vertical align="center">
|
||||
<Stack.Item>
|
||||
<GiantLever />
|
||||
</Stack.Item>
|
||||
<Stack.Item>Flush</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack vertical align="center">
|
||||
<Stack.Item>
|
||||
<RoundGauge
|
||||
size={2}
|
||||
value={pressure}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
ranges={{
|
||||
bad: [0, 50],
|
||||
average: [50, 98],
|
||||
good: [98, 100],
|
||||
}}
|
||||
format={(val) => ''}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Lights state={stateText} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<Stack align="flex-end" justify="space-around" mt={3}>
|
||||
<Stack.Item>
|
||||
<EjectButton />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Stack vertical align="center">
|
||||
<Stack.Item>
|
||||
<EStopSvg
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
filter: mode
|
||||
? 'drop-shadow(5px 5px 2px rgba(0, 0, 0, 1)'
|
||||
: 'none',
|
||||
transform: mode ? 'none' : 'rotate(45deg)',
|
||||
transition:
|
||||
'0.2s filter linear, 0.2s -webkit-filter linear, 0.2s transform linear',
|
||||
}}
|
||||
onClick={() => act(mode ? 'pumpOff' : 'pumpOn')}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item bold>E-Stop</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const EjectButton = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
const { isAI } = data;
|
||||
const [buttonPressed, setButtonPressed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!buttonPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setButtonPressed(false);
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [buttonPressed]);
|
||||
|
||||
return (
|
||||
<Stack vertical align="center">
|
||||
<Stack.Item>
|
||||
<Box
|
||||
backgroundColor={isAI ? '#1a1a1a' : '#3a3a3a'}
|
||||
p={1}
|
||||
style={{
|
||||
boxShadow: buttonPressed ? 'none' : '5px 5px 2px #000000',
|
||||
cursor: 'pointer',
|
||||
transition: '0.2s all ease-in',
|
||||
}}
|
||||
onClick={() => {
|
||||
act('eject');
|
||||
setButtonPressed(true);
|
||||
}}
|
||||
>
|
||||
<Icon color={isAI ? '#5a5a5a' : '#ffffff'} name="eject" size={3} />
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item bold>Eject</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const GiantLever = (props) => {
|
||||
const { act, data } = useBackend<Data>();
|
||||
|
||||
return (
|
||||
<Box position="relative" width={5} height={6}>
|
||||
{/* Track 1 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={1.5}
|
||||
height={6}
|
||||
width={2}
|
||||
backgroundColor="#262728"
|
||||
style={{
|
||||
border: '6px solid #3b3d38',
|
||||
}}
|
||||
/>
|
||||
{/* Lever */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={1.4}
|
||||
left={2.2}
|
||||
height={2}
|
||||
width={0.6}
|
||||
backgroundColor="#9f9f9f"
|
||||
style={{
|
||||
borderRadius: '10px',
|
||||
transition: 'transform .3s cubic-bezier(0.600, 0.040, 0.980, 0.335)',
|
||||
transitionDelay: '.2s',
|
||||
transform: data.flushing ? 'translateY(20px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
{/* Handle */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0.5}
|
||||
left={0}
|
||||
height={1.4}
|
||||
width={5}
|
||||
onClick={() => act(data.flushing ? 'disengageHandle' : 'engageHandle')}
|
||||
style={{
|
||||
background: 'linear-gradient(180deg,rgb(141, 17, 13), #200e0d)',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform .5s cubic-bezier(0.600, 0.040, 0.980, 0.335)',
|
||||
transform: data.flushing ? 'translateY(43px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Lights = (props: { state: string }) => {
|
||||
const { state } = props;
|
||||
return (
|
||||
<Stack vertical>
|
||||
<Stack.Item>
|
||||
<Box
|
||||
height={2}
|
||||
width={2}
|
||||
style={{
|
||||
background:
|
||||
state === 'Pressurizing'
|
||||
? 'linear-gradient(90deg,rgb(190, 54, 54), #482214)'
|
||||
: 'linear-gradient(90deg, #1c0f0d, #130b0a)',
|
||||
transition: 'background 0.5s linear',
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box
|
||||
height={2}
|
||||
width={2}
|
||||
style={{
|
||||
background:
|
||||
state === 'Ready'
|
||||
? 'linear-gradient(90deg, #4a6c14, #164b20)'
|
||||
: 'linear-gradient(90deg, #24310e, #122a15)',
|
||||
transition: 'background 0.5s linear',
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const EStopSvg = (props: React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="15mm"
|
||||
height="15mm"
|
||||
viewBox="0 0 100 100"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<marker
|
||||
id="b"
|
||||
markerHeight={2}
|
||||
markerUnits="userSpaceOnUse"
|
||||
markerWidth={2}
|
||||
orient="auto-start-reverse"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
refX={0}
|
||||
refY={0}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
}}
|
||||
viewBox="0 0 1 1"
|
||||
>
|
||||
<path
|
||||
d="m5.77 0-8.65 5V-5Z"
|
||||
style={{
|
||||
fill: 'context-stroke',
|
||||
fillRule: 'evenodd',
|
||||
stroke: 'context-stroke',
|
||||
strokeWidth: '1pt',
|
||||
}}
|
||||
transform="scale(.5)"
|
||||
/>
|
||||
</marker>
|
||||
<marker
|
||||
id="c"
|
||||
markerHeight={2}
|
||||
markerUnits="userSpaceOnUse"
|
||||
markerWidth={2}
|
||||
orient="auto-start-reverse"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
refX={0}
|
||||
refY={0}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
}}
|
||||
viewBox="0 0 1 1"
|
||||
>
|
||||
<path
|
||||
d="m5.77 0-8.65 5V-5Z"
|
||||
style={{
|
||||
fill: 'context-stroke',
|
||||
fillRule: 'evenodd',
|
||||
stroke: 'context-stroke',
|
||||
strokeWidth: '1pt',
|
||||
}}
|
||||
transform="scale(.5)"
|
||||
/>
|
||||
</marker>
|
||||
<marker
|
||||
id="a"
|
||||
markerHeight={2}
|
||||
markerUnits="userSpaceOnUse"
|
||||
markerWidth={2}
|
||||
orient="auto-start-reverse"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
refX={0}
|
||||
refY={0}
|
||||
style={{
|
||||
overflow: 'visible',
|
||||
}}
|
||||
viewBox="0 0 1 1"
|
||||
>
|
||||
<path
|
||||
d="m5.77 0-8.65 5V-5Z"
|
||||
style={{
|
||||
fill: 'context-stroke',
|
||||
fillRule: 'evenodd',
|
||||
stroke: 'context-stroke',
|
||||
strokeWidth: '1pt',
|
||||
}}
|
||||
transform="scale(.5)"
|
||||
/>
|
||||
</marker>
|
||||
</defs>
|
||||
<circle
|
||||
cx={50}
|
||||
cy={50}
|
||||
r={50}
|
||||
style={{
|
||||
fill: '#b2362d',
|
||||
fillOpacity: 1,
|
||||
strokeWidth: 1.5875,
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M84.641 70A40 40 0 0 1 30 84.641"
|
||||
style={{
|
||||
fill: 'none',
|
||||
fillOpacity: 1,
|
||||
stroke: '#fff',
|
||||
strokeWidth: 5,
|
||||
strokeDasharray: 'none',
|
||||
strokeOpacity: 1,
|
||||
markerEnd: 'url(#a)',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M15.386 70.027a40 40 0 0 1 14.641-54.64"
|
||||
style={{
|
||||
display: 'inline',
|
||||
fill: 'none',
|
||||
fillOpacity: 1,
|
||||
stroke: '#fff',
|
||||
strokeWidth: 5,
|
||||
strokeDasharray: 'none',
|
||||
strokeOpacity: 1,
|
||||
markerEnd: 'url(#b)',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M50.027 10.027a40 40 0 0 1 40 40"
|
||||
style={{
|
||||
display: 'inline',
|
||||
fill: 'none',
|
||||
fillOpacity: 1,
|
||||
stroke: '#fff',
|
||||
strokeWidth: 5,
|
||||
strokeDasharray: 'none',
|
||||
strokeOpacity: 1,
|
||||
markerEnd: 'url(#c)',
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useBackend } from 'tgui/backend';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import {
|
||||
AnimatedNumber,
|
||||
Box,
|
||||
Icon,
|
||||
LabeledList,
|
||||
ProgressBar,
|
||||
Section,
|
||||
@@ -14,11 +17,11 @@ type Data = {
|
||||
totalOutput: number;
|
||||
maxTotalOutput: number;
|
||||
thermalOutput: number;
|
||||
primary: circulator;
|
||||
secondary: circulator;
|
||||
primary: Circulator;
|
||||
secondary: Circulator;
|
||||
};
|
||||
|
||||
type circulator = {
|
||||
type Circulator = {
|
||||
dir: string;
|
||||
output: number;
|
||||
flowCapacity: number;
|
||||
@@ -35,7 +38,7 @@ export const TEGenerator = (props) => {
|
||||
data;
|
||||
|
||||
return (
|
||||
<Window width={550} height={310}>
|
||||
<Window width={550} height={350}>
|
||||
<Window.Content>
|
||||
<Section title="Status">
|
||||
<LabeledList>
|
||||
@@ -69,7 +72,7 @@ export const TEGenerator = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const TEGCirculator = (props: { name: string; values: circulator }) => {
|
||||
const TEGCirculator = (props: { name: string; values: Circulator }) => {
|
||||
const { name, values } = props;
|
||||
|
||||
const {
|
||||
@@ -84,26 +87,79 @@ const TEGCirculator = (props: { name: string; values: circulator }) => {
|
||||
|
||||
return (
|
||||
<Section title={name + ' (' + dir + ')'}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Turbine Output">
|
||||
{formatPower(output)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Flow Capacity">
|
||||
{toFixed(flowCapacity, 2)}%
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Inlet Pressure">
|
||||
{formatSiUnit(inletPressure * 1000, 0, 'Pa')}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Inlet Temperature">
|
||||
{toFixed(inletTemperature, 2)} K
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Outlet Pressure">
|
||||
{formatSiUnit(outletPressure * 1000, 0, 'Pa')}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Outlet Temperature">
|
||||
{toFixed(outletTemperature, 2)} K
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
<Stack vertical fill>
|
||||
<Stack.Item>
|
||||
<Stack align="center" fill justify="space-around">
|
||||
<Stack.Item>
|
||||
<CirculatorFanSpinner value={flowCapacity} />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Box color="label" fontSize={1.2}>
|
||||
Flow Capacity
|
||||
</Box>
|
||||
<Box>
|
||||
<AnimatedNumber
|
||||
value={flowCapacity}
|
||||
format={(val) => val.toFixed(2) + '%'}
|
||||
/>
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Divider />
|
||||
<Stack.Item>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Turbine Output">
|
||||
{formatPower(output)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Inlet Pressure">
|
||||
{formatSiUnit(inletPressure * 1000, 0, 'Pa')}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Inlet Temperature">
|
||||
{toFixed(inletTemperature, 2)} K
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Outlet Pressure">
|
||||
{formatSiUnit(outletPressure * 1000, 0, 'Pa')}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Outlet Temperature">
|
||||
{toFixed(outletTemperature, 2)} K
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const CirculatorFanSpinner = (props: { value: number }) => {
|
||||
const { value } = props;
|
||||
|
||||
const [rotation, setRotation] = useState(0);
|
||||
|
||||
const SPEED_MULTIPLIER = 0.2;
|
||||
const FLOW_MODIFIER = 4;
|
||||
const STEP_SIZE = 4;
|
||||
|
||||
useEffect(() => {
|
||||
// Only spin if there's ~some~ flow.
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const id = setInterval(
|
||||
() => {
|
||||
setRotation((rot) => (rot + STEP_SIZE) % 359);
|
||||
},
|
||||
SPEED_MULTIPLIER * (100 * FLOW_MODIFIER - value * FLOW_MODIFIER),
|
||||
);
|
||||
return () => clearInterval(id);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Icon
|
||||
rotation={rotation}
|
||||
size={4}
|
||||
name="fan"
|
||||
color={value > 80 ? 'good' : value > 50 ? 'average' : 'bad'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user