[MIRROR] Pretty animations and UI overhaul for RIGSuits (#10743)

Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-04-25 18:28:49 -07:00
committed by GitHub
parent 9b233f14b1
commit b10094c44e
9 changed files with 401 additions and 115 deletions

View File

@@ -803,6 +803,8 @@
/obj/item/rig/dropped(mob/user)
. = ..(user)
// So the next user will see the boot animation
tgui_shared_states.Cut()
for(var/piece in list("helmet","gauntlets","chest","boots"))
toggle_piece(piece, user, ONLY_RETRACT)
if(wearer && wearer.wearing_rig == src)

View File

@@ -1,3 +1,6 @@
// Just used to force the icon into the rsc, Byond.iconRefMap does the rest
GLOBAL_DATUM_INIT(rigsuit_ui_icon, /icon, 'icons/hud/rig_ui_slots.dmi')
/*
* This defines the global UI for RIGSuits.
* It has all of the relevant TGUI procs, but it's entry point is in rig_verbs.dm

BIN
icons/hud/rig_ui_slots.dmi Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,5 +1,13 @@
import { useBackend } from 'tgui/backend';
import { Button, LabeledList, Section } from 'tgui-core/components';
import {
Box,
DmIcon,
Icon,
Section,
Stack,
Tooltip,
} from 'tgui-core/components';
import { type BooleanLike } from 'tgui-core/react';
import { capitalize } from 'tgui-core/string';
import type { Data } from './types';
@@ -8,6 +16,9 @@ export const RIGSuitHardware = (props) => {
const { act, data } = useBackend<Data>();
const {
sealed,
aioverride,
cooling,
// Disables buttons while the suit is busy
sealing,
// Each piece
@@ -23,68 +34,212 @@ export const RIGSuitHardware = (props) => {
return (
<Section title="Hardware">
<LabeledList>
<LabeledList.Item
label="Helmet"
buttons={
<Button
icon={helmetDeployed ? 'sign-out-alt' : 'sign-in-alt'}
disabled={sealing}
selected={helmetDeployed}
onClick={() => act('toggle_piece', { piece: 'helmet' })}
>
{helmetDeployed ? 'Deployed' : 'Deploy'}
</Button>
}
<Stack fill align="center" justify="space-around">
<Stack.Item grow>
<Stack align="center" justify="center">
<Stack.Item>
<Box
className="RIGSuit__FadeIn"
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px',
width: 'fit-content',
}}
textColor="#fff"
>
<HardwarePiece
disabled={sealing}
icon="head"
name={helmet}
selected={helmetDeployed}
onClick={() => act('toggle_piece', { piece: 'helmet' })}
/>
<HardwarePiece
disabled={sealing}
icon="body"
name={chest}
selected={chestDeployed}
onClick={() => act('toggle_piece', { piece: 'chest' })}
/>
<HardwarePiece
disabled={sealing}
icon="gloves"
name={gauntlets}
selected={gauntletsDeployed}
onClick={() => act('toggle_piece', { piece: 'gauntlets' })}
/>
<HardwarePiece
disabled={sealing}
icon="boots"
name={boots}
selected={bootsDeployed}
onClick={() => act('toggle_piece', { piece: 'boots' })}
/>
</Box>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item
style={{
borderLeft: '2px solid #246984',
borderRight: '2px solid #246984',
}}
height={15}
width={0}
>
{helmet ? capitalize(helmet) : 'ERROR'}
</LabeledList.Item>
<LabeledList.Item
label="Gauntlets"
buttons={
<Button
icon={gauntletsDeployed ? 'sign-out-alt' : 'sign-in-alt'}
disabled={sealing}
selected={gauntletsDeployed}
onClick={() => act('toggle_piece', { piece: 'gauntlets' })}
>
{gauntletsDeployed ? 'Deployed' : 'Deploy'}
</Button>
}
>
{gauntlets ? capitalize(gauntlets) : 'ERROR'}
</LabeledList.Item>
<LabeledList.Item
label="Boots"
buttons={
<Button
icon={bootsDeployed ? 'sign-out-alt' : 'sign-in-alt'}
disabled={sealing}
selected={bootsDeployed}
onClick={() => act('toggle_piece', { piece: 'boots' })}
>
{bootsDeployed ? 'Deployed' : 'Deploy'}
</Button>
}
>
{boots ? capitalize(boots) : 'ERROR'}
</LabeledList.Item>
<LabeledList.Item
label="Chestpiece"
buttons={
<Button
icon={chestDeployed ? 'sign-out-alt' : 'sign-in-alt'}
disabled={sealing}
selected={chestDeployed}
onClick={() => act('toggle_piece', { piece: 'chest' })}
>
{chestDeployed ? 'Deployed' : 'Deploy'}
</Button>
}
>
{chest ? capitalize(chest) : 'ERROR'}
</LabeledList.Item>
</LabeledList>
&nbsp;
</Stack.Item>
<Stack.Item grow>
<Stack fill align="center" justify="center">
<Stack.Item>
<Box
className="RIGSuit__FadeIn"
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px',
width: 'fit-content',
}}
>
<Tooltip
content={
'Suit Seals: ' + sealing
? 'Sealing'
: sealed
? 'Sealed'
: 'UNSEALED'
}
>
<Box
position="relative"
className="RIGSuit__Icon"
style={{
filter: sealing
? 'grayscale(100%) brightness(0.5)'
: sealed
? undefined
: 'grayscale(80%)',
}}
onClick={() => act('toggle_seals')}
>
<DmIcon
height={6}
width={6}
icon="icons/hud/rig_ui_slots.dmi"
icon_state="base"
/>
<Box position="absolute" top={0.5} left={0.5}>
<Icon name="power-off" size={5} color="#5c9fd8" />
</Box>
</Box>
</Tooltip>
<Tooltip
content={
aioverride
? 'Toggle AI Control Off'
: 'Toggle AI Control On'
}
>
<Box
position="relative"
className="RIGSuit__Icon"
style={{
filter: aioverride ? undefined : 'grayscale(80%)',
}}
onClick={() => act('toggle_ai_control')}
>
<DmIcon
height={6}
width={6}
icon="icons/hud/rig_ui_slots.dmi"
icon_state="base"
/>
<Box position="absolute" top={0.7} left={0.5}>
<Icon name="robot" size={4} color="#5c9fd8" />
</Box>
</Box>
</Tooltip>
<Tooltip content={cooling ? 'Cooling: On' : 'Cooling: Off'}>
<Box
position="relative"
className="RIGSuit__Icon"
style={{
filter: cooling ? undefined : 'grayscale(80%)',
}}
onClick={() => act('toggle_cooling')}
>
<DmIcon
height={6}
width={6}
icon="icons/hud/rig_ui_slots.dmi"
icon_state="base"
/>
<Box position="absolute" top={1} left={1}>
<Icon name="wind" size={4} color="#5c9fd8" />
</Box>
</Box>
</Tooltip>
<Tooltip content="Tank Settings">
<Box
position="relative"
className="RIGSuit__Icon"
onClick={() => act('tank_settings')}
>
<DmIcon
height={6}
width={6}
icon="icons/hud/rig_ui_slots.dmi"
icon_state="base"
/>
<Box position="absolute" top={1} left={0.5}>
<Icon name="lungs" size={4} color="#5c9fd8" />
</Box>
</Box>
</Tooltip>
</Box>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const HardwarePiece = (props: {
name: string;
icon: string;
selected: BooleanLike;
disabled: BooleanLike;
onClick: () => void;
}) => {
let filter;
if (props.disabled) {
filter = 'grayscale(100%) brightness(0.5)';
} else if (props.selected) {
filter = undefined;
} else {
filter = 'grayscale(80%)';
}
return (
<Tooltip content={capitalize(props.name) || 'ERROR'}>
<Box className="RIGSuit__Icon">
<DmIcon
height={6}
width={6}
icon="icons/hud/rig_ui_slots.dmi"
icon_state={props.icon}
style={{
cursor: 'pointer',
filter,
}}
onClick={props.onClick}
/>
</Box>
</Tooltip>
);
};

View File

@@ -0,0 +1,118 @@
import { useEffect, useState } from 'react';
import { Window } from 'tgui/layouts';
import { Box, Stack } from 'tgui-core/components';
// This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
// http://creativecommons.org/licenses/by-sa/4.0/
const NTLogoReact = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.0"
viewBox="0 0 425 200"
fill="#00bbbb"
>
<path
className="RIGSuit__FadeIn"
d="m 178.00399,0.03869 -71.20393,0 a 6.7613422,6.0255495 0 0 0 -6.76134,6.02555 l 0,187.87147 a 6.7613422,6.0255495 0 0 0 6.76134,6.02554 l 53.1072,0 a 6.7613422,6.0255495 0 0 0 6.76135,-6.02554 l 0,-101.544018 72.21628,104.699398 a 6.7613422,6.0255495 0 0 0 5.76015,2.87016 l 73.55487,0 a 6.7613422,6.0255495 0 0 0 6.76135,-6.02554 l 0,-187.87147 a 6.7613422,6.0255495 0 0 0 -6.76135,-6.02555 l -54.71644,0 a 6.7613422,6.0255495 0 0 0 -6.76133,6.02555 l 0,102.61935 L 183.76413,2.90886 a 6.7613422,6.0255495 0 0 0 -5.76014,-2.87017 z"
/>
<path
className="RIGSuit__Animation__WipeLeft"
style={{ animationDelay: '1s' }}
d="M 4.8446333,22.10875 A 13.412039,12.501842 0 0 1 13.477588,0.03924 l 66.118315,0 a 5.3648158,5.000737 0 0 1 5.364823,5.00073 l 0,79.87931 z"
/>
<path
className="RIGSuit__Animation__WipeLeft"
style={{ animationDelay: '2s' }}
d="m 420.15535,177.89119 a 13.412038,12.501842 0 0 1 -8.63295,22.06951 l -66.11832,0 a 5.3648152,5.000737 0 0 1 -5.36482,-5.00074 l 0,-79.87931 z"
/>
</svg>
);
};
const LoadingText = (props: { onFinish?: () => void }) => {
const onFinish = props.onFinish ? props.onFinish : () => {};
useEffect(() => {
const timer = setTimeout(() => {
onFinish();
}, 5000);
return () => {
clearTimeout(timer);
};
}, []);
return (
<Box>
<Box className="RIGSuit__Animation__WipeLeft">RigOS Loading...</Box>
<Box
className="RIGSuit__Animation__WipeLeft"
style={{ animationDelay: '1s' }}
>
Starting Power-On Self Test...
</Box>
<Box
className="RIGSuit__Animation__WipeLeft"
style={{ animationDelay: '2s' }}
>
POST:{' '}
<Box inline color="good">
GOOD.
</Box>
</Box>
<Box
className="RIGSuit__Animation__WipeLeft"
style={{ animationDelay: '3s' }}
>
Loading UI...
</Box>
<Box
className="RIGSuit__Animation__WipeLeft"
style={{ animationDelay: '4s' }}
>
All Systems Ready!
</Box>
</Box>
);
};
export const LoaderNT = (props: { onFinish?: () => void }) => {
const [showLogo, setShowLogo] = useState(true);
useEffect(() => {
setTimeout(() => {
setShowLogo(false);
}, 3500);
}, []);
if (showLogo) {
return (
<Stack vertical fill justify="center">
<Stack.Item>
<NTLogoReact />
</Stack.Item>
</Stack>
);
}
return <LoadingText onFinish={props.onFinish} />;
};
export const RIGSuitLoader = (props: { onFinish?: () => void }) => {
// You can skip to the end by clicking
const onFinish = props.onFinish ? props.onFinish : () => {};
return (
<Window height={400} width={600}>
<Window.Content
fitted
onClick={() => onFinish()}
style={{ cursor: 'pointer' }}
>
<Box backgroundColor="black" height="100%">
<LoaderNT onFinish={props.onFinish} />
</Box>
</Window.Content>
</Window>
);
};

View File

@@ -5,7 +5,6 @@ import {
LabeledList,
ProgressBar,
Section,
Stack,
} from 'tgui-core/components';
import type { Data } from './types';
@@ -31,57 +30,7 @@ export const RIGSuitStatus = (props) => {
} = data;
return (
<Section
title="Status"
buttons={
<Stack>
<Stack.Item>
<Button
icon={sealing ? 'redo' : sealed ? 'power-off' : 'lock-open'}
iconSpin={sealing}
disabled={sealing}
selected={sealed}
onClick={() => act('toggle_seals')}
>
{'Suit ' +
(sealing
? 'seals working...'
: sealed
? 'is Active'
: 'is Inactive')}
</Button>
</Stack.Item>
<Stack.Item>
<Button
icon="robot"
selected={aioverride}
onClick={() => act('toggle_ai_control')}
tooltip={'AI Control ' + (aioverride ? 'Enabled' : 'Disabled')}
tooltipPosition="bottom-end"
/>
</Stack.Item>
<Stack.Item>
<Button
icon="wind"
selected={cooling}
onClick={() => act('toggle_cooling')}
tooltip={
'Suit Cooling ' + (cooling ? 'is Active' : 'is Inactive')
}
tooltipPosition="bottom-end"
/>
</Stack.Item>
<Stack.Item>
<Button
icon="lungs"
onClick={() => act('tank_settings')}
tooltip="Tank Settings"
tooltipPosition="bottom-end"
/>
</Stack.Item>
</Stack>
}
>
<Section title="Status">
<LabeledList>
<LabeledList.Item label="Power Supply">
<ProgressBar

View File

@@ -1,8 +1,9 @@
import { useBackend } from 'tgui/backend';
import { useBackend, useSharedState } from 'tgui/backend';
import { Window } from 'tgui/layouts';
import { Box } from 'tgui-core/components';
import { RIGSuitHardware } from './RIGSuitHardware';
import { RIGSuitLoader } from './RIGSuitLoader';
import { RIGSuitModules } from './RIGSuitModules';
import { RIGSuitStatus } from './RIGSuitStatus';
import type { Data } from './types';
@@ -12,6 +13,12 @@ export const RIGSuit = (props) => {
const { interfacelock, malf, aicontrol, ai } = data;
const [showLoading, setShowLoading] = useSharedState('rigsuit-loading', true);
if (showLoading) {
return <RIGSuitLoader onFinish={() => setShowLoading(false)} />;
}
let override: React.JSX.Element | null = null;
if (interfacelock || malf) {

View File

@@ -0,0 +1,51 @@
@keyframes fade-in {
from {
opacity: 0.1;
}
to {
opacity: 1;
}
}
.RIGSuit__FadeIn {
animation: 2s linear fade-in forwards;
opacity: 0.1;
}
.RIGSuit__Name {
animation: 1.5s linear wipe-in-left both;
animation-delay: 1.8s;
}
@keyframes wipe-in-left {
from {
clip-path: inset(0 100% 0 0);
}
to {
clip-path: inset(0 0 0 0);
}
}
.RIGSuit__Line {
animation: 1s linear wipe-in-left both;
animation-delay: 1s;
}
.RIGSuit__Animation__WipeLeft {
animation: 1s linear wipe-in-left both;
}
.RIGSuit__Icon {
cursor: pointer;
transition:
0.2s filter linear,
0.2s -webkit-filter linear;
user-select: none;
}
.RIGSuit__Icon > img {
transition:
0.2s filter linear,
0.2s -webkit-filter linear;
-webkit-user-drag: none;
}

View File

@@ -41,6 +41,7 @@
@include meta.load-css('./interfaces/Pda.scss');
@include meta.load-css('./interfaces/ResearchConsole.scss');
@include meta.load-css('./interfaces/ResleevingConsole.scss');
@include meta.load-css('./interfaces/RIGSuit.scss');
@include meta.load-css('./interfaces/Roulette.scss');
@include meta.load-css('./interfaces/Safe.scss');
@include meta.load-css('./interfaces/TachyonArray.scss');