This commit is contained in:
Letter N
2021-01-31 10:16:48 +08:00
parent 512124100e
commit 02bba115a6
104 changed files with 5086 additions and 986 deletions
+5 -5
View File
@@ -684,11 +684,11 @@ rules:
## expressions.
# react/jsx-child-element-spacing: error
## Validate closing bracket location in JSX (fixable)
react/jsx-closing-bracket-location: [error, {
## NOTE: Not really sure about enforcing this one
selfClosing: false,
nonEmpty: after-props,
}]
# react/jsx-closing-bracket-location: [error, {
# ## NOTE: Not really sure about enforcing this one
# selfClosing: false,
# nonEmpty: after-props,
# }]
## Validate closing tag location in JSX (fixable)
react/jsx-closing-tag-location: error
## Enforce or disallow newlines inside of curly braces in JSX attributes and
+1
View File
@@ -18,3 +18,4 @@ bin/tgui text eol=lf
## Treat bundles as binary and ignore them during conflicts
*.bundle.* binary merge=tgui-merge-bundle
*.chunk.* binary merge=tgui-merge-bundle
.yarn/releases/* binary
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
yarnPath: ".yarn/releases/yarn-berry.js"
yarnPath: .yarn/releases/yarn-2.3.3.cjs
+1 -1
View File
@@ -38,7 +38,7 @@ to translate concepts between old and new tgui, read this
You will need these programs to start developing in tgui:
- [Node v12.18.3+](https://nodejs.org/en/download/)
- [Yarn v1.22.4+](https://yarnpkg.com/en/docs/install) (optional)
- [Yarn v1.22.4+](https://yarnpkg.com/getting-started/install) (optional)
- [Git Bash](https://git-scm.com/downloads)
or [MSys2](https://www.msys2.org/) (optional)
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
## Copyright (c) 2020 Aleksej Komarov
## SPDX-License-Identifier: MIT
@@ -28,7 +28,7 @@ base_dir="$(tgui-realpath "${base_dir}")"
## Fall back to running Yarn from the repo
if ! hash yarn 2>/dev/null; then
alias yarn="node '${base_dir}/.yarn/releases/yarn-berry.js'"
alias yarn="node '${base_dir}/.yarn/releases/yarn-2.3.3.cjs'"
fi
+1 -1
View File
@@ -15,7 +15,7 @@ Set-Location $basedir
## --------------------------------------------------------
function yarn {
node.exe ".yarn\releases\yarn-berry.js" @Args
node.exe ".yarn\releases\yarn-2.3.3.cjs" @Args
}
function Remove-Quiet {
+58 -3
View File
@@ -28,6 +28,7 @@ Make sure to add new items to this list if you document new components.
- [`Grid`](#grid)
- [`Grid.Column`](#gridcolumn)
- [`Icon`](#icon)
- [`Icon.Stack`](#iconstack)
- [`Input`](#input)
- [`Knob`](#knob)
- [`LabeledControls`](#labeledcontrols)
@@ -39,6 +40,7 @@ Make sure to add new items to this list if you document new components.
- [`NoticeBox`](#noticebox)
- [`NumberInput`](#numberinput)
- [`ProgressBar`](#progressbar)
- [`RoundGauge`](#roundgauge)
- [`Section`](#section)
- [`Slider`](#slider)
- [`Table`](#table)
@@ -246,7 +248,7 @@ A button with an extra confirmation step, using native button component.
**Props:**
- See inherited props: [Button](#button)
- `confirmMessage: string` - Text to display after first click; defaults to "Confirm?"
- `confirmContent: string` - Text to display after first click; defaults to "Confirm?"
- `confirmColor: string` - Color to display after first click; defaults to "bad"
### `Button.Input`
@@ -359,12 +361,16 @@ and displays selected entry.
**Props:**
- See inherited props: [Box](#box)
- See inherited props: [Icon](#icon)
- `options: string[]` - An array of strings which will be displayed in the
dropdown when open
- `selected: string` - Currently selected entry
- `width: number` - Width of dropdown button and resulting menu
- `over: boolean` - dropdown renders over instead of below
- `color: string` - color of dropdown button
- `over: boolean` - Dropdown renders over instead of below
- `color: string` - Color of dropdown button
- `nochevron: boolean` - Whether or not the arrow on the right hand side of the dropdown button is visible
- `noscroll: boolean` - Whether or not the dropdown menu should have a scroll bar
- `displayText: string` - Text to always display in place of the selected text
- `onClick: (e) => void` - Called when dropdown button is clicked
- `onSelected: (value) => void` - Called when a value is picked from the list, `value` is the value that was picked
@@ -529,6 +535,22 @@ Fractional numbers are supported.
- `spin: boolean` - Whether an icon should be spinning. Good for load
indicators.
### `Icon.Stack`
Renders children icons on top of each other in order to make your own icon.
```jsx
<Icon.Stack>
<Icon name="pen" />
<Icon name="slash" />
</Icon.Stack>
```
**Props:**
- See inherited props: [Box](#box)
- `children: Icon` - Icons to stack.
### `Input`
A basic text input, which allow users to enter text into a UI.
@@ -754,6 +776,38 @@ based on whether the value lands in the range between `from` and `to`.
- `color: string` - Color of the progress bar.
- `children: any` - Content to render inside the progress bar.
### `RoundGauge`
The RoundGauge component provides a visual representation of a single metric, as well as being capable of showing informational or cautionary boundaries related to that metric.
```jsx
<RoundGauge
size={1.75}
value={tankPressure}
minValue={0}
maxValue={pressureLimit}
alertAfter={pressureLimit * 0.70}
ranges={{
"good": [0, pressureLimit * 0.70],
"average": [pressureLimit * 0.70, pressureLimit * 0.85],
"bad": [pressureLimit * 0.85, pressureLimit],
}}
format={formatPressure} />
```
The alert on the gauge is optional, and will only be shown if the `alertAfter` prop is defined. When defined, the alert will begin to flash the respective color upon which the needle currently rests, as defined in the `ranges` prop.
**Props:**
- See inherited props: [Box](#box)
- `value: number` - The current value of the metric.
- `minValue: number` (default: 0) - The lower bound of the guage.
- `maxValue: number` (default: 1) - The upper bound of the guage.
- `ranges: { color: [from, to] }` (default: `{ "good": [0, 1] }`) - Provide regions of the guage to color between two specified values of the metric.
- `alertAfter: number` (optional) - When provided, will cause an alert symbol on the gauge to begin flashing in the color upon which the needle currently rest, as defined in `ranges`.
- `format: function(value) => string` (optional) - When provided, will be used to format the value of the metric for display.
- `size: number` (default: 1) - When provided scales the gauge.
### `Section`
Section is a surface that displays content and actions on a single topic.
@@ -993,6 +1047,7 @@ Example:
- For a list of themes, see `packages/tgui/styles/themes`.
- `title: string` - Window title.
- `resizable: boolean` - Controls resizability of the window.
- `noClose: boolean` - Controls the ability to close the window.
- `children: any` - Child elements, which are rendered directly inside the
window. If you use a [Dimmer](#dimmer) or [Modal](#modal) in your UI,
they should be put as direct childs of a Window, otherwise you should be
+14 -6
View File
@@ -76,7 +76,8 @@ input. The input's `action` and `params` are passed to the proc.
```dm
/obj/machinery/my_machine/ui_act(action, params)
if(..())
. = ..()
if(.)
return
if(action == "change_color")
var/new_color = params["color"]
@@ -88,13 +89,20 @@ input. The input's `action` and `params` are passed to the proc.
```
The `..()` (parent call) is very important here, as it is how we check that the
user is allowed to use this interface (to avoid so-called href exploits). It is
also very important to clamp and sanitize all input here. Always assume the user
is attempting to exploit the game.
user is allowed to use this interface (to avoid so-called href exploits). When
any event has been handled `..()` will return `TRUE`. It is important to clamp
and sanitize all input here. Always assume the user is attempting to exploit the
game.
When `..()` has returned `TRUE` your interface can safely assume that the user's
action has been handled already by some parent proc and you should not continue
to handle this, instead preserving and returning the parent proc's return value.
Also note the use of `. = TRUE` (or `FALSE`), which is used to notify the UI
that this input caused an update. This is especially important for UIs that do
not auto-update, as otherwise the user will never see their change.
that this input has been handled. When `ui_act` eventually returns, a value of
`TRUE` indicates that the input has been handled and that the UI should update.
This is important for UIs that do not auto-update, as otherwise the user will
not be able to see the interface update based on thier actions.
### Frontend
+13 -14
View File
@@ -6,31 +6,30 @@
"packages/*"
],
"dependencies": {
"@babel/core": "^7.6.2",
"@babel/plugin-transform-jscript": "^7.2.0",
"@babel/preset-env": "^7.6.2",
"@babel/core": "^7.11.1",
"@babel/plugin-transform-jscript": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"babel-plugin-inferno": "^6.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-inferno": "^6.1.1",
"babel-plugin-transform-remove-console": "^6.9.4",
"common": "workspace:*",
"css-loader": "^3.2.0",
"css-loader": "^4.2.1",
"cssnano": "^4.1.10",
"eslint": "^7.4.0",
"eslint-plugin-react": "^7.17.0",
"extract-css-chunks-webpack-plugin": "^4.6.0",
"extract-css-chunks-webpack-plugin": "^4.7.5",
"file-loader": "^6.0.0",
"inferno": "^7.4.2",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"pnp-webpack-plugin": "^1.6.4",
"sass": "^1.22.12",
"sass-loader": "^9.0.2",
"style-loader": "^1.0.0",
"terser-webpack-plugin": "^3.0.6",
"sass": "^1.26.10",
"sass-loader": "^9.0.3",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^4.1.0",
"url-loader": "^4.1.0",
"webpack": "^4.40.2",
"webpack-build-notifier": "^2.0.0",
"webpack-bundle-analyzer": "^3.5.1",
"webpack": "^4.44.1",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12"
}
}
+2 -2
View File
@@ -73,7 +73,7 @@ export const toKeyedArray = (obj, keyProp = 'key') => {
* @returns {any[]}
*/
export const filter = iterateeFn => collection => {
if (collection === null && collection === undefined) {
if (collection === null || collection === undefined) {
return collection;
}
if (Array.isArray(collection)) {
@@ -100,7 +100,7 @@ export const filter = iterateeFn => collection => {
* @returns {any[]}
*/
export const map = iterateeFn => collection => {
if (collection === null && collection === undefined) {
if (collection === null || collection === undefined) {
return collection;
}
if (Array.isArray(collection)) {
+10
View File
@@ -88,3 +88,13 @@ export const keyOfMatchingRange = (value, ranges) => {
}
}
};
/**
* Get number of digits following the decimal point in a number
*/
export const numberOfDecimalDigits = value => {
if (Math.floor(value) !== value) {
return value.toString().split('.')[1].length || 0;
}
return 0;
};
+5 -5
View File
@@ -3,13 +3,13 @@
"name": "tgui-dev-server",
"version": "4.2.0",
"dependencies": {
"axios": "^0.21.1",
"axios": "^0.19.2",
"common": "workspace:*",
"esm": "^3.2.25",
"glob": "^7.1.4",
"glob": "^7.1.6",
"source-map": "^0.7.3",
"stacktrace-parser": "^0.1.7",
"webpack": "^4.40.2",
"ws": "^7.1.2"
"stacktrace-parser": "^0.1.10",
"webpack": "^4.44.1",
"ws": "^7.3.1"
}
}
+2 -2
View File
@@ -33,7 +33,7 @@ export const Panel = (props, context) => {
}
}
return (
<Pane theme={settings.theme === 'default' ? 'light' : settings.theme}>
<Pane theme={settings.theme}>
<Flex
direction="column"
height="100%">
@@ -116,7 +116,7 @@ export const Panel = (props, context) => {
const HoboPanel = (props, context) => {
const settings = useSettings(context);
return (
<Pane theme={settings.theme === 'default' ? 'light' : settings.theme}>
<Pane theme={settings.theme}>
<Pane.Content scrollable>
<Button
style={{
+1 -2
View File
@@ -7,12 +7,11 @@
import { storage } from 'common/storage';
import { loadSettings, updateSettings } from '../settings/actions';
import { selectSettings } from '../settings/selectors';
import { addChatPage, changeChatPage, changeScrollTracking, loadChat, rebuildChat, toggleAcceptedType, updateMessageCount, removeChatPage, saveChatToDisk } from './actions';
import { addChatPage, changeChatPage, changeScrollTracking, loadChat, rebuildChat, removeChatPage, saveChatToDisk, toggleAcceptedType, updateMessageCount } from './actions';
import { MAX_PERSISTED_MESSAGES, MESSAGE_SAVE_INTERVAL } from './constants';
import { createMessage, serializeMessage } from './model';
import { chatRenderer } from './renderer';
import { selectChat, selectCurrentChatPage } from './selectors';
import { logger } from 'tgui/logging';
const saveChatToStorage = async store => {
const state = selectChat(store.getState());
+4 -1
View File
@@ -313,7 +313,10 @@ class ChatRenderer {
}
}
// Linkify text
linkifyNode(node);
const linkifyNodes = node.querySelectorAll('.linkify');
for (let i = 0; i < linkifyNodes.length; ++i) {
linkifyNode(linkifyNodes[i]);
}
// Assign an image error handler
if (now < message.createdAt + IMAGE_RETRY_MESSAGE_AGE) {
const imgNodes = node.querySelectorAll('img');
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: MIT
*/
em {
em {
font-style: normal;
font-weight: bold;
}
@@ -255,7 +255,7 @@ a.popt {
/* ADD HERE FOR ITALIC */
.italic, .italics {
.italic, .italics, .emote {
font-style: italic;
}
@@ -418,7 +418,7 @@ em {
}
.emote {
font-style: italic;
}
.userdanger {
@@ -475,12 +475,22 @@ em {
}
.tinynotice {
color: #6685f5;
font-size: 85%;
}
.tinynoticeital {
color: #6685f5;
font-style: italic;
font-size: 85%;
}
.smallnotice {
color: #6685f5;
font-size: 90%;
}
.smallnoticeital {
color: #6685f5;
font-style: italic;
font-size: 90%;
@@ -545,40 +555,40 @@ em {
}
.cult {
color: #aa1c1c;
color: #973e3b;
}
.cultitalic {
color: #aa1c1c;
color: #973e3b;
font-style: italic;
}
.cultbold {
color: #aa1c1c;
color: #973e3b;
font-style: italic;
font-weight: bold;
}
.cultboldtalic {
color: #aa1c1c;
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.cultlarge {
color: #aa1c1c;
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.narsie {
color: #aa1c1c;
color: #973e3b;
font-weight: bold;
font-size: 925%;
}
.narsiesmall {
color: #aa1c1c;
color: #973e3b;
font-weight: bold;
font-size: 370%;
}
@@ -655,7 +665,7 @@ em {
}
.deconversion_message {
color: #5000A0;
color: #a947ff;
font-size: 185%;
font-style: italic;
}
@@ -273,7 +273,8 @@ a.popt {
.italic,
.italics {
.italics,
.emote {
font-style: italic;
}
@@ -442,6 +443,10 @@ h1.alert, h2.alert {
color: #000000;
}
.emote {
font-style: italic;
}
.userdanger {
color: #ff0000;
font-weight: bold;
@@ -511,6 +516,11 @@ h1.alert, h2.alert {
}
.tinynotice {
color: #000099;
font-size: 85%;
}
.tinynoticeital {
color: #000099;
font-style: italic;
font-size: 85%;
@@ -586,40 +596,40 @@ h1.alert, h2.alert {
}
.cult {
color: #960000;
color: #973e3b;
}
.cultitalic {
color: #960000;
color: #973e3b;
font-style: italic;
}
.cultbold {
color: #960000;
color: #973e3b;
font-style: italic;
font-weight: bold;
}
.cultboldtalic {
color: #960000;
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.cultlarge {
color: #960000;
color: #973e3b;
font-weight: bold;
font-size: 185%;
}
.narsie {
color: #960000;
color: #973e3b;
font-weight: bold;
font-size: 925%;
}
.narsiesmall {
color: #960000;
color: #973e3b;
font-weight: bold;
font-size: 370%;
}
@@ -678,6 +688,7 @@ h1.alert, h2.alert {
font-size: 185%;
}
/* compatability */
.umbra {
color: #5000A0;
}
@@ -881,7 +892,7 @@ h1.alert, h2.alert {
.command_headset {
font-weight: bold;
font-size: 125%;
font-size: 125%; /* 160% default, changed so command can scream less */
}
.small {
@@ -917,7 +928,7 @@ h1.alert, h2.alert {
.clown {
color: #FF69Bf;
font-size: 125%;
font-size: 125%; /* 160% default, changed so "command" can scream less */
font-family: "Comic Sans MS", cursive, sans-serif;
font-weight: bold;
}
@@ -933,7 +944,7 @@ h1.alert, h2.alert {
font-style: italic;
}
.spooky {
.spooky { /* cit specific, used by jacq */
color: #FF9100;
}
@@ -966,7 +977,7 @@ h1.alert, h2.alert {
}
}
.velvet {
.velvet { /* cit specific, used by MKU */
color: #660015;
font-weight: bold;
animation: velvet 5000ms infinite;
+65
View File
@@ -0,0 +1,65 @@
import { Component } from 'inferno';
const DEFAULT_BLINKING_INTERVAL = 1000;
const DEFAULT_BLINKING_TIME = 1000;
export class Blink extends Component {
constructor() {
super();
this.state = {
hidden: false,
};
}
createTimer() {
const {
interval = DEFAULT_BLINKING_INTERVAL,
time = DEFAULT_BLINKING_TIME,
} = this.props;
clearInterval(this.interval);
clearTimeout(this.timer);
this.setState({
hidden: false,
});
this.interval = setInterval(() => {
this.setState({
hidden: true,
});
this.timer = setTimeout(() => {
this.setState({
hidden: false,
});
}, time);
}, interval + time);
}
componentDidMount() {
this.createTimer();
}
componentDidUpdate(prevProps) {
if (prevProps.interval !== this.props.interval
|| prevProps.time !== this.props.time) {
this.createTimer();
}
}
componentWillUnmount() {
clearInterval(this.interval);
clearTimeout(this.timer);
}
render(props) {
return (
<span style={{
visibility: this.state.hidden ? "hidden" : "visible",
}}>
{props.children}
</span>
);
}
}
+17 -2
View File
@@ -4,9 +4,9 @@
* @license MIT
*/
import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from 'common/keycodes';
import { classes, pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from '../hotkeys';
import { createLogger } from '../logging';
import { Box } from './Box';
import { Icon } from './Icon';
@@ -21,11 +21,14 @@ export const Button = props => {
icon,
iconRotation,
iconSpin,
iconColor,
iconPosition,
color,
disabled,
selected,
tooltip,
tooltipPosition,
tooltipOverrideLong,
ellipsis,
compact,
circular,
@@ -57,6 +60,7 @@ export const Button = props => {
ellipsis && 'Button--ellipsis',
circular && 'Button--circular',
compact && 'Button--compact',
iconPosition && 'Button--iconPosition--' + iconPosition,
(color && typeof color === 'string')
? 'Button--color--' + color
: 'Button--color--default',
@@ -86,17 +90,26 @@ export const Button = props => {
}
}}
{...rest}>
{icon && (
{(icon && iconPosition !== 'right') && (
<Icon
name={icon}
color={iconColor}
rotation={iconRotation}
spin={iconSpin} />
)}
{content}
{children}
{(icon && iconPosition === 'right') && (
<Icon
name={icon}
color={iconColor}
rotation={iconRotation}
spin={iconSpin} />
)}
{tooltip && (
<Tooltip
content={tooltip}
overrideLong={tooltipOverrideLong}
position={tooltipPosition} />
)}
</Box>
@@ -221,6 +234,7 @@ export class ButtonInput extends Component {
iconSpin,
tooltip,
tooltipPosition,
tooltipOverrideLong,
color = 'default',
placeholder,
maxLength,
@@ -270,6 +284,7 @@ export class ButtonInput extends Component {
{tooltip && (
<Tooltip
content={tooltip}
overrideLong={tooltipOverrideLong}
position={tooltipPosition}
/>
)}
@@ -173,6 +173,7 @@ export class DraggableControl extends Component {
unit,
minValue,
maxValue,
unclamped,
format,
onChange,
onDrag,
@@ -221,10 +222,16 @@ export class DraggableControl extends Component {
if (!editing) {
return;
}
const value = clamp(
parseFloat(e.target.value),
minValue,
maxValue);
let value;
if (unclamped) {
value = parseFloat(e.target.value);
}
else {
value = clamp(
parseFloat(e.target.value),
minValue,
maxValue);
}
if (Number.isNaN(value)) {
this.setState({
editing: false,
@@ -245,10 +252,16 @@ export class DraggableControl extends Component {
}}
onKeyDown={e => {
if (e.keyCode === 13) {
const value = clamp(
parseFloat(e.target.value),
minValue,
maxValue);
let value;
if (unclamped) {
value = parseFloat(e.target.value);
}
else {
value = clamp(
parseFloat(e.target.value),
minValue,
maxValue);
}
if (Number.isNaN(value)) {
this.setState({
editing: false,
+12 -1
View File
@@ -64,6 +64,9 @@ export class Dropdown extends Component {
render() {
const { props } = this;
const {
icon,
iconRotation,
iconSpin,
color = 'default',
over,
noscroll,
@@ -72,6 +75,7 @@ export class Dropdown extends Component {
onClick,
selected,
disabled,
displayText,
...boxProps
} = props;
const {
@@ -114,8 +118,15 @@ export class Dropdown extends Component {
}
this.setOpen(!this.state.open);
}}>
{icon && (
<Icon
name={icon}
rotation={iconRotation}
spin={iconSpin}
mr={1} />
)}
<span className="Dropdown__selected-text">
{this.state.selected}
{displayText ? displayText : this.state.selected}
</span>
{!!nochevron || (
<span className="Dropdown__arrow-button">
+1 -1
View File
@@ -33,7 +33,7 @@ export const computeFlexProps = props => {
style: {
...rest.style,
'flex-direction': direction,
'flex-wrap': wrap,
'flex-wrap': wrap === true ? 'wrap' : wrap,
'align-items': align,
'justify-content': justify,
},
+36 -1
View File
@@ -1,6 +1,8 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @author Original Aleksej Komarov
* @author Changes ThePotato97
* @license MIT
*/
@@ -10,7 +12,16 @@ import { Box } from './Box';
const FA_OUTLINE_REGEX = /-o$/;
export const Icon = props => {
const { name, size, spin, className, style = {}, rotation, ...rest } = props;
const {
name,
size,
spin,
className,
style = {},
rotation,
inverse,
...rest
} = props;
if (size) {
style['font-size'] = (size * 100) + '%';
}
@@ -23,6 +34,7 @@ export const Icon = props => {
<Box
as="i"
className={classes([
'Icon',
className,
faRegular ? 'far' : 'fas',
'fa-' + faName,
@@ -34,3 +46,26 @@ export const Icon = props => {
};
Icon.defaultHooks = pureComponentHooks;
export const IconStack = props => {
const {
className,
style = {},
children,
...rest
} = props;
return (
<Box
as="span"
class={classes([
'IconStack',
className,
])}
style={style}
{...rest}>
{children}
</Box>
);
};
Icon.Stack = IconStack;
+5 -3
View File
@@ -24,6 +24,7 @@ export const Knob = props => {
format,
maxValue,
minValue,
unclamped,
onChange,
onDrag,
step,
@@ -50,6 +51,7 @@ export const Knob = props => {
format,
maxValue,
minValue,
unclamped,
onChange,
onDrag,
step,
@@ -79,7 +81,7 @@ export const Knob = props => {
const effectiveColor = color
|| keyOfMatchingRange(fillValue ?? value, ranges)
|| 'default';
const rotation = (scaledDisplayValue - 0.5) * 270;
const rotation = Math.min((scaledDisplayValue - 0.5) * 270, 225);
return (
<div
className={classes([
@@ -127,8 +129,8 @@ export const Knob = props => {
className="Knob__ringFill"
style={{
'stroke-dashoffset': (
((bipolar ? 2.75 : 2.00) - scaledFillValue * 1.5)
* Math.PI * 50
Math.max(((bipolar ? 2.75 : 2.00) - scaledFillValue * 1.5)
* Math.PI * 50, 0)
),
}}
cx="50"
@@ -9,11 +9,13 @@ import { Flex } from './Flex';
export const LabeledControls = props => {
const {
children,
wrap,
...rest
} = props;
return (
<Flex
mx={-0.5}
wrap={wrap}
align="stretch"
justify="space-between"
{...rest}>
@@ -26,12 +28,12 @@ const LabeledControlsItem = props => {
const {
label,
children,
mx = 1,
...rest
} = props;
return (
<Flex.Item mx={1}>
<Flex.Item mx={mx}>
<Flex
minWidth="52px"
height="100%"
direction="column"
align="center"
+125
View File
@@ -0,0 +1,125 @@
/**
* @file
* @copyright 2020 bobbahbrown (https://github.com/bobbahbrown)
* @license MIT
*/
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
import { AnimatedNumber } from './AnimatedNumber';
import { Box, computeBoxClassName, computeBoxProps } from './Box';
export const RoundGauge = props => {
// Support for IE8 is for losers sorry B)
if (Byond.IS_LTE_IE8) {
return (
<AnimatedNumber {...props} />
);
}
const {
value,
minValue = 1,
maxValue = 1,
ranges,
alertAfter,
format,
size = 1,
className,
style,
...rest
} = props;
const scaledValue = scale(
value,
minValue,
maxValue);
const clampedValue = clamp01(scaledValue);
let scaledRanges = ranges ? {} : { "primary": [0, 1] };
if (ranges)
{ Object.keys(ranges).forEach(x => {
const range = ranges[x];
scaledRanges[x] = [
scale(range[0], minValue, maxValue),
scale(range[1], minValue, maxValue),
];
}); }
let alertColor = null;
if (alertAfter < value) {
alertColor = keyOfMatchingRange(clampedValue, scaledRanges);
}
return (
<Box inline>
<div
className={classes([
'RoundGauge',
className,
computeBoxClassName(rest),
])}
{...computeBoxProps({
style: {
'font-size': size + 'em',
...style,
},
...rest,
})}>
<svg
viewBox="0 0 100 50">
{alertAfter && (
<g className={classes([
'RoundGauge__alert',
alertColor ? `active RoundGauge__alert--${alertColor}` : '',
])}>
<path d="M48.211,14.578C48.55,13.9 49.242,13.472 50,13.472C50.758,13.472 51.45,13.9 51.789,14.578C54.793,20.587 60.795,32.589 63.553,38.106C63.863,38.726 63.83,39.462 63.465,40.051C63.101,40.641 62.457,41 61.764,41C55.996,41 44.004,41 38.236,41C37.543,41 36.899,40.641 36.535,40.051C36.17,39.462 36.137,38.726 36.447,38.106C39.205,32.589 45.207,20.587 48.211,14.578ZM50,34.417C51.426,34.417 52.583,35.574 52.583,37C52.583,38.426 51.426,39.583 50,39.583C48.574,39.583 47.417,38.426 47.417,37C47.417,35.574 48.574,34.417 50,34.417ZM50,32.75C50,32.75 53,31.805 53,22.25C53,20.594 51.656,19.25 50,19.25C48.344,19.25 47,20.594 47,22.25C47,31.805 50,32.75 50,32.75Z" />
</g>
)}
<g>
<circle
className="RoundGauge__ringTrack"
cx="50"
cy="50"
r="45" />
</g>
<g>
{Object.keys(scaledRanges).map((x, i) => {
const col_ranges = scaledRanges[x];
return (
<circle
className={`RoundGauge__ringFill RoundGauge--color--${x}`}
key={i}
style={{
'stroke-dashoffset': (
Math.max((2.0 - (col_ranges[1] - col_ranges[0]))
* Math.PI * 50, 0)
),
}}
transform={`rotate(${180 + 180 * col_ranges[0]} 50 50)`}
cx="50"
cy="50"
r="45" />
);
})}
</g>
<g
className="RoundGauge__needle"
transform={`rotate(${clampedValue * 180 - 90} 50 50)`}>
<polygon
className="RoundGauge__needleLine"
points="46,50 50,0 54,50" />
<circle
className="RoundGauge__needleMiddle"
cx="50"
cy="50"
r="8" />
</g>
</svg>
</div>
<AnimatedNumber
value={value}
format={format}
size={size} />
</Box>
);
};
+11 -8
View File
@@ -41,10 +41,18 @@ export class Section extends Component {
...rest
} = this.props;
const hasTitle = canRender(title) || canRender(buttons);
const hasContent = canRender(children);
const content = fitted
? children
: (
<div
ref={this.ref}
className="Section__content">
{children}
</div>
);
return (
<div
ref={this.ref}
ref={fitted ? this.ref : undefined}
className={classes([
'Section',
'Section--level--' + level,
@@ -66,12 +74,7 @@ export class Section extends Component {
</div>
</div>
)}
{fitted && children
|| hasContent && (
<div className="Section__content">
{children}
</div>
)}
{content}
</div>
);
}
+3 -1
View File
@@ -135,6 +135,7 @@ export class TextArea extends Component {
onBlur,
onEnter,
value,
maxLength,
placeholder,
...boxProps
} = this.props;
@@ -161,7 +162,8 @@ export class TextArea extends Component {
onKeyPress={this.handleKeyPress}
onInput={this.handleOnInput}
onFocus={this.handleFocus}
onBlur={this.handleBlur} />
onBlur={this.handleBlur}
maxLength={maxLength} />
</Box>
);
}
+3 -1
View File
@@ -9,11 +9,13 @@ import { classes } from 'common/react';
export const Tooltip = props => {
const {
content,
overrideLong = false,
position = 'bottom',
} = props;
// Empirically calculated length of the string,
// at which tooltip text starts to overflow.
const long = typeof content === 'string' && content.length > 35;
const long = typeof content === 'string'
&& (content.length > 35 && !overrideLong);
return (
<div
className={classes([
+2
View File
@@ -5,6 +5,7 @@
*/
export { AnimatedNumber } from './AnimatedNumber';
export { Blink } from './Blink';
export { BlockQuote } from './BlockQuote';
export { Box } from './Box';
export { Button } from './Button';
@@ -27,6 +28,7 @@ export { Modal } from './Modal';
export { NoticeBox } from './NoticeBox';
export { NumberInput } from './NumberInput';
export { ProgressBar } from './ProgressBar';
export { RoundGauge } from './RoundGauge';
export { Section } from './Section';
export { Slider } from './Slider';
export { Table } from './Table';
+15 -1
View File
@@ -6,7 +6,7 @@
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { BlockQuote, Box, Button, ByondUi, Collapsible, DraggableControl, Flex, Icon, Input, Knob, LabeledList, NoticeBox, NumberInput, ProgressBar, Section, Slider, Tabs, Tooltip } from '../components';
import { Blink, BlockQuote, Box, Button, ByondUi, Collapsible, DraggableControl, Flex, Icon, Input, Knob, LabeledList, NoticeBox, NumberInput, ProgressBar, Section, Slider, Tabs, Tooltip } from '../components';
import { formatSiUnit } from '../format';
import { Pane, Window } from '../layouts';
import { createLogger } from '../logging';
@@ -41,6 +41,10 @@ const PAGES = [
title: 'Button',
component: () => KitchenSinkButton,
},
{
title: 'Blink',
component: () => KitchenSinkBlinker,
},
{
title: 'Box',
component: () => KitchenSinkBox,
@@ -175,6 +179,16 @@ const KitchenSinkButton = props => {
);
};
const KitchenSinkBlinker = props => {
return (
<Section>
<Blink>
Blink
</Blink>
</Section>
);
};
const KitchenSinkBox = props => {
return (
<Section>
+4
View File
@@ -178,6 +178,8 @@ export const dragStartHandler = event => {
window.screenLeft - event.screenX,
window.screenTop - event.screenY,
];
// Focus click target
event.target?.focus();
document.addEventListener('mousemove', dragMoveHandler);
document.addEventListener('mouseup', dragEndHandler);
dragMoveHandler(event);
@@ -214,6 +216,8 @@ export const resizeStartHandler = (x, y) => event => {
window.innerWidth,
window.innerHeight,
];
// Focus click target
event.target?.focus();
document.addEventListener('mousemove', resizeMoveHandler);
document.addEventListener('mouseup', resizeEndHandler);
resizeMoveHandler(event);
-1
View File
@@ -8,7 +8,6 @@
import { EventEmitter } from 'common/events';
import { KEY_ALT, KEY_CTRL, KEY_F1, KEY_F12, KEY_SHIFT } from 'common/keycodes';
import { logger } from './logging';
export const globalEvents = new EventEmitter();
let ignoreWindowFocus = false;
+59
View File
@@ -25,6 +25,11 @@ const SI_SYMBOLS = [
'E', // exa
'Z', // zetta
'Y', // yotta
'R', // ronna
'Q', // quecca
'F',
'N',
'H',
];
const SI_BASE_INDEX = SI_SYMBOLS.indexOf(' ');
@@ -110,3 +115,57 @@ export const formatDb = value => {
}
return sign + formatted + ' dB';
};
const SI_BASE_TEN_UNIT = [
'',
'· 10³', // kilo
'· 10⁶', // mega
'· 10⁹', // giga
'· 10¹²', // tera
'· 10¹⁵', // peta
'· 10¹⁸', // exa
'· 10²¹', // zetta
'· 10²⁴', // yotta
'· 10²⁷', // ronna
'· 10³⁰', // quecca
'· 10³³',
'· 10³⁶',
'· 10³⁹',
];
const SI_BASE_TEN_INDEX = SI_BASE_TEN_UNIT.indexOf(' ');
/**
* Formats a number to a human readable form, by reducing it to SI units.
* TODO: This is quite a shit code and shit math, needs optimization.
*/
export const formatSiBaseTenUnit = (
value,
minBase1000 = -SI_BASE_TEN_INDEX,
unit = ''
) => {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return value;
}
const realBase10 = Math.floor(Math.log10(value));
const base10 = Math.floor(Math.max(minBase1000 * 3, realBase10));
const realBase1000 = Math.floor(realBase10 / 3);
const base1000 = Math.floor(base10 / 3);
const symbolIndex = clamp(
SI_BASE_TEN_INDEX + base1000,
0,
SI_BASE_TEN_UNIT.length);
const symbol = SI_BASE_TEN_UNIT[symbolIndex];
const scaledNumber = value / Math.pow(1000, base1000);
const scaledPrecision = realBase1000 > minBase1000
? (2 + base1000 * 3 - base10)
: 0;
// TODO: Make numbers bigger than precision value show
// up to 2 decimal numbers.
const finalString = (
toFixed(scaledNumber, scaledPrecision)
+ ' ' + symbol + ' ' + unit
);
return finalString.trim();
};
+1
View File
@@ -8,6 +8,7 @@
import './styles/main.scss';
import './styles/themes/abductor.scss';
import './styles/themes/cardtable.scss';
import './styles/themes/clockcult.scss';
import './styles/themes/hackerman.scss';
import './styles/themes/malfunction.scss';
import './styles/themes/neutral.scss';
@@ -1,8 +1,8 @@
import { GenericUplink } from './Uplink';
import { Fragment } from 'inferno';
import { useBackend, useSharedState } from '../backend';
import { Button, LabeledList, NoticeBox, Section, Tabs } from '../components';
import { Fragment } from 'inferno';
import { Window } from '../layouts';
import { GenericUplink } from './Uplink';
export const AbductorConsole = (props, context) => {
const [tab, setTab] = useSharedState(context, 'tab', 1);
+2 -4
View File
@@ -1,12 +1,10 @@
import { toFixed } from 'common/math';
import { decodeHtmlEntities } from 'common/string';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, LabeledList, NumberInput, Section } from '../components';
import { getGasLabel } from '../constants';
import { Box, Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
import { Scrubber, Vent } from './common/AtmosControls';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
import { Vent, Scrubber } from './common/AtmosControls';
export const AirAlarm = (props, context) => {
const { act, data } = useBackend(context);
@@ -0,0 +1,73 @@
/**
* @file
* @copyright 2020 bobbahbrown (https://github.com/bobbahbrown)
* @license MIT
*/
import { clamp01 } from 'common/math';
import { useBackend } from '../backend';
import { Box, Button, Flex } from '../components';
import { Window } from '../layouts';
export const AlertModal = (props, context) => {
const { act, data } = useBackend(context);
const {
title,
message,
buttons,
timeout,
} = data;
return (
<Window
title={title}
width={350}
height={150}
resizable>
{timeout !== undefined && <Loader value={timeout} />}
<Window.Content>
<Flex direction="column" height="100%">
<Flex.Item grow={1}>
<Flex
direction="column"
className="AlertModal__Message"
height="100%">
<Flex.Item>
<Box m={1}>
{message}
</Box>
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item my={2}>
<Flex className="AlertModal__Buttons">
{buttons.map(button => (
<Flex.Item key={button} mx={1}>
<Button
px={3}
onClick={() => act("choose", { choice: button })}>
{button}
</Button>
</Flex.Item>
))}
</Flex>
</Flex.Item>
</Flex>
</Window.Content>
</Window>
);
};
export const Loader = props => {
const { value } = props;
return (
<div
className="AlertModal__Loader">
<Box
className="AlertModal__LoaderProgress"
style={{
width: clamp01(value) * 100 + '%',
}} />
</div>
);
};
+2 -1
View File
@@ -1,9 +1,10 @@
import { map, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { pureComponentHooks } from 'common/react';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Flex, Icon, Table, Tabs } from '../components';
import { Fragment, Window } from '../layouts';
import { Window } from '../layouts';
import { AreaCharge, powerRank } from './PowerMonitor';
export const ApcControl = (props, context) => {
@@ -17,7 +17,7 @@ export const Biogenerator = (props, context) => {
return (
<Window
width={550}
height={380}
height={420}
resizable>
{!!processing && (
<Dimmer fontSize="32px">
@@ -135,16 +135,16 @@ const ItemList = (props, context) => {
hoveredItem,
setHoveredItem,
] = useLocalState(context, 'hoveredItem', {});
const hoveredCost = hoveredItem && hoveredItem.cost || 0;
const hoveredCost = hoveredItem.cost || 0;
// Append extra hover data to items
const items = props.items.map(item => {
const [
amount,
setAmount,
] = useLocalState(context, "amount" + item.name, 1);
const notSameItem = hoveredItem && hoveredItem.name !== item.name;
const notSameItem = hoveredItem.name !== item.name;
const notEnoughHovered = props.biomass - hoveredCost
* hoveredItem.amount < item.cost * amount;
* hoveredItem.amount < item.cost * amount;
const disabledDueToHovered = notSameItem && notEnoughHovered;
const disabled = props.biomass < item.cost * amount || disabledDueToHovered;
return {
+40 -32
View File
@@ -2,16 +2,15 @@ import { filter, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { classes } from 'common/react';
import { createSearch } from 'common/string';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Button, ByondUi, Input, Section } from '../components';
import { Button, ByondUi, Flex, Input, Section } from '../components';
import { Window } from '../layouts';
/**
* Returns previous and next camera names relative to the currently
* active camera.
*/
const prevNextCamera = (cameras, activeCamera) => {
export const prevNextCamera = (cameras, activeCamera) => {
if (!activeCamera) {
return [];
}
@@ -29,7 +28,7 @@ const prevNextCamera = (cameras, activeCamera) => {
*
* Filters cameras, applies search terms and sorts the alphabetically.
*/
const selectCameras = (cameras, searchText = '') => {
export const selectCameras = (cameras, searchText = '') => {
const testSearch = createSearch(searchText, camera => camera.name);
return flow([
// Null camera filter
@@ -42,7 +41,7 @@ const selectCameras = (cameras, searchText = '') => {
};
export const CameraConsole = (props, context) => {
const { act, data, config } = useBackend(context);
const { act, data } = useBackend(context);
const { mapRef, activeCamera } = data;
const cameras = selectCameras(data.cameras);
const [
@@ -100,36 +99,45 @@ export const CameraConsoleContent = (props, context) => {
const { activeCamera } = data;
const cameras = selectCameras(data.cameras, searchText);
return (
<Fragment>
<Input
autoFocus
fluid
mb={1}
placeholder="Search for a camera"
onInput={(e, value) => setSearchText(value)} />
<Section>
{cameras.map(camera => (
<Flex
direction={"column"}
height="100%">
<Flex.Item>
<Input
autoFocus
fluid
mt={1}
placeholder="Search for a camera"
onInput={(e, value) => setSearchText(value)} />
</Flex.Item>
<Flex.Item
height="100%">
<Section
fill
scrollable>
{cameras.map(camera => (
// We're not using the component here because performance
// would be absolutely abysmal (50+ ms for each re-render).
<div
key={camera.name}
title={camera.name}
className={classes([
'Button',
'Button--fluid',
'Button--color--transparent',
'Button--ellipsis',
activeCamera
<div
key={camera.name}
title={camera.name}
className={classes([
'Button',
'Button--fluid',
'Button--color--transparent',
'Button--ellipsis',
activeCamera
&& camera.name === activeCamera.name
&& 'Button--selected',
])}
onClick={() => act('switch_camera', {
name: camera.name,
})}>
{camera.name}
</div>
))}
</Section>
</Fragment>
])}
onClick={() => act('switch_camera', {
name: camera.name,
})}>
{camera.name}
</div>
))}
</Section>
</Flex.Item>
</Flex>
);
};
+157 -125
View File
@@ -1,10 +1,17 @@
import { toFixed } from 'common/math';
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, Icon, Knob, LabeledControls, LabeledList, Section, Tooltip } from '../components';
import { Box, Button, Flex, Icon, Knob, LabeledControls, LabeledList, RoundGauge, Section, Tooltip } from '../components';
import { formatSiUnit } from '../format';
import { Window } from '../layouts';
const formatPressure = value => {
if (value < 10000) {
return toFixed(value) + ' kPa';
}
return formatSiUnit(value * 1000, 1, 'Pa');
};
export const Canister = (props, context) => {
const { act, data } = useBackend(context);
const {
@@ -14,142 +21,167 @@ export const Canister = (props, context) => {
defaultReleasePressure,
minReleasePressure,
maxReleasePressure,
pressureLimit,
valveOpen,
isPrototype,
hasHoldingTank,
holdingTank,
holdingTankLeakPressure,
holdingTankFragPressure,
restricted,
} = data;
return (
<Window
width={300}
height={232}>
height={275}>
<Window.Content>
<Section
title="Canister"
buttons={(
<Fragment>
{!!isPrototype && (
<Button
<Flex direction="column" height="100%">
<Flex.Item mb={1}>
<Section
title="Canister"
buttons={(
<Fragment>
{!!isPrototype && (
<Button
mr={1}
icon={restricted ? 'lock' : 'unlock'}
color="caution"
content={restricted
? 'Engineering'
: 'Public'}
onClick={() => act('restricted')} />
)}
<Button
icon="pencil-alt"
content="Relabel"
onClick={() => act('relabel')} />
</Fragment>
)}>
<LabeledControls>
<LabeledControls.Item
minWidth="66px"
label="Pressure">
<RoundGauge
size={1.75}
value={tankPressure}
minValue={0}
maxValue={pressureLimit}
alertAfter={pressureLimit * 0.70}
ranges={{
"good": [0, pressureLimit * 0.70],
"average": [pressureLimit * 0.70, pressureLimit * 0.85],
"bad": [pressureLimit * 0.85, pressureLimit],
}}
format={formatPressure} />
</LabeledControls.Item>
<LabeledControls.Item label="Regulator">
<Box
position="relative"
left="-8px">
<Knob
size={1.25}
color={!!valveOpen && 'yellow'}
value={releasePressure}
unit="kPa"
minValue={minReleasePressure}
maxValue={maxReleasePressure}
step={5}
stepPixelSize={1}
onDrag={(e, value) => act('pressure', {
pressure: value,
})} />
<Button
fluid
position="absolute"
top="-2px"
right="-20px"
color="transparent"
icon="fast-forward"
onClick={() => act('pressure', {
pressure: maxReleasePressure,
})} />
<Button
fluid
position="absolute"
top="16px"
right="-20px"
color="transparent"
icon="undo"
onClick={() => act('pressure', {
pressure: defaultReleasePressure,
})} />
</Box>
</LabeledControls.Item>
<LabeledControls.Item label="Valve">
<Button
my={0.5}
width="50px"
lineHeight={2}
fontSize="11px"
color={valveOpen
? (hasHoldingTank ? 'caution' : 'danger')
: null}
content={valveOpen ? 'Open' : 'Closed'}
onClick={() => act('valve')} />
</LabeledControls.Item>
<LabeledControls.Item
mr={1}
icon={restricted ? 'lock' : 'unlock'}
color="caution"
content={restricted
? 'Engineering'
: 'Public'}
onClick={() => act('restricted')} />
label="Port">
<Box position="relative">
<Icon
size={1.25}
name={portConnected ? 'plug' : 'times'}
color={portConnected ? 'good' : 'bad'} />
<Tooltip
content={portConnected
? 'Connected'
: 'Disconnected'}
position="top" />
</Box>
</LabeledControls.Item>
</LabeledControls>
</Section>
</Flex.Item>
<Flex.Item grow={1}>
<Section
height="100%"
title="Holding Tank"
buttons={!!hasHoldingTank && (
<Button
icon="eject"
color={valveOpen && 'danger'}
content="Eject"
onClick={() => act('eject')} />
)}>
{!!hasHoldingTank && (
<LabeledList>
<LabeledList.Item label="Label">
{holdingTank.name}
</LabeledList.Item>
<LabeledList.Item label="Pressure">
<RoundGauge
value={holdingTank.tankPressure}
minValue={0}
maxValue={holdingTankFragPressure * 1.15}
alertAfter={holdingTankLeakPressure}
ranges={{
"good": [0, holdingTankLeakPressure],
"average": [holdingTankLeakPressure, holdingTankFragPressure],
"bad": [holdingTankFragPressure, holdingTankFragPressure * 1.15],
}}
format={formatPressure}
size={1.75} />
</LabeledList.Item>
</LabeledList>
)}
<Button
icon="pencil-alt"
content="Relabel"
onClick={() => act('relabel')} />
</Fragment>
)}>
<LabeledControls>
<LabeledControls.Item
minWidth="66px"
label="Pressure">
<AnimatedNumber
value={tankPressure}
format={value => {
if (value < 10000) {
return toFixed(value) + ' kPa';
}
return formatSiUnit(value * 1000, 1, 'Pa');
}} />
</LabeledControls.Item>
<LabeledControls.Item label="Regulator">
<Box
position="relative"
left="-8px">
<Knob
size={1.25}
color={!!valveOpen && 'yellow'}
value={releasePressure}
unit="kPa"
minValue={minReleasePressure}
maxValue={maxReleasePressure}
step={5}
stepPixelSize={1}
onDrag={(e, value) => act('pressure', {
pressure: value,
})} />
<Button
fluid
position="absolute"
top="-2px"
right="-20px"
color="transparent"
icon="fast-forward"
onClick={() => act('pressure', {
pressure: maxReleasePressure,
})} />
<Button
fluid
position="absolute"
top="16px"
right="-20px"
color="transparent"
icon="undo"
onClick={() => act('pressure', {
pressure: defaultReleasePressure,
})} />
</Box>
</LabeledControls.Item>
<LabeledControls.Item label="Valve">
<Button
my={0.5}
width="50px"
lineHeight={2}
fontSize="11px"
color={valveOpen
? (hasHoldingTank ? 'caution' : 'danger')
: null}
content={valveOpen ? 'Open' : 'Closed'}
onClick={() => act('valve')} />
</LabeledControls.Item>
<LabeledControls.Item
mr={1}
label="Port">
<Box position="relative">
<Icon
size={1.25}
name={portConnected ? 'plug' : 'times'}
color={portConnected ? 'good' : 'bad'} />
<Tooltip
content={portConnected
? 'Connected'
: 'Disconnected'}
position="top" />
</Box>
</LabeledControls.Item>
</LabeledControls>
</Section>
<Section
title="Holding Tank"
buttons={!!hasHoldingTank && (
<Button
icon="eject"
color={valveOpen && 'danger'}
content="Eject"
onClick={() => act('eject')} />
)}>
{!!hasHoldingTank && (
<LabeledList>
<LabeledList.Item label="Label">
{holdingTank.name}
</LabeledList.Item>
<LabeledList.Item label="Pressure">
<AnimatedNumber value={holdingTank.tankPressure} /> kPa
</LabeledList.Item>
</LabeledList>
)}
{!hasHoldingTank && (
<Box color="average">
No Holding Tank
</Box>
)}
</Section>
{!hasHoldingTank && (
<Box color="average">
No Holding Tank
</Box>
)}
</Section>
</Flex.Item>
</Flex>
</Window.Content>
</Window>
);
+9 -6
View File
@@ -3,7 +3,7 @@ import { useBackend } from '../backend';
import { Box, Button } from '../components';
import { Window } from '../layouts';
const PX_PER_UNIT = 28;
const PX_PER_UNIT = 24;
class PaintCanvas extends Component {
constructor(props) {
@@ -60,14 +60,15 @@ class PaintCanvas extends Component {
const {
res = 1,
value,
dotsize = PX_PER_UNIT,
...rest
} = this.props;
const [width, height] = getImageSize(value);
return (
<canvas
ref={this.canvasRef}
width={(width * PX_PER_UNIT) || 300}
height={(height * PX_PER_UNIT) || 300}
width={(width * dotsize) || 300}
height={(height * dotsize) || 300}
{...rest}
onClick={e => this.clickwrapper(e)}>
Canvas failed to render.
@@ -84,16 +85,18 @@ const getImageSize = value => {
export const Canvas = (props, context) => {
const { act, data } = useBackend(context);
const dotsize = PX_PER_UNIT;
const [width, height] = getImageSize(data.grid);
return (
<Window
width={Math.min(400, width * PX_PER_UNIT * 32 + 24)}
height={Math.min(400, height * PX_PER_UNIT * 32 + 24)}
width={Math.min(700, width * dotsize + 72)}
height={Math.min(700, height * dotsize + 72)}
resizable>
<Window.Content scrollable>
<Window.Content>
<Box textAlign="center">
<PaintCanvas
value={data.grid}
dotsize={dotsize}
onCanvasClick={(x, y) => act("paint", { x, y })} />
<Box>
{!data.finalized && (
@@ -1,6 +1,6 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, Flex, LabeledList, NoticeBox, Section } from '../components';
import { Button, Flex, LabeledList, NoticeBox, Section } from '../components';
import { Window } from '../layouts';
export const CivCargoHoldTerminal = (props, context) => {
@@ -0,0 +1,721 @@
import { sortBy } from "common/collections";
import { capitalize } from "common/string";
import { useBackend, useLocalState } from "../backend";
import { Blink, Box, Button, Dimmer, Flex, Icon, Input, Modal, Section, TextArea } from "../components";
import { Window } from "../layouts";
import { sanitizeText } from "../sanitize";
const STATE_BUYING_SHUTTLE = "buying_shuttle";
const STATE_CHANGING_STATUS = "changing_status";
const STATE_MAIN = "main";
const STATE_MESSAGES = "messages";
// Used for whether or not you need to swipe to confirm an alert level change
const SWIPE_NEEDED = "SWIPE_NEEDED";
const sortByCreditCost = sortBy(shuttle => shuttle.creditCost);
const AlertButton = (props, context) => {
const { act, data } = useBackend(context);
const { alertLevelTick, canSetAlertLevel } = data;
const { alertLevel, setShowAlertLevelConfirm } = props;
const thisIsCurrent = data.alertLevel === alertLevel;
return (
<Button
icon="exclamation-triangle"
color={thisIsCurrent && "good"}
content={capitalize(alertLevel)}
onClick={() => {
if (thisIsCurrent) {
return;
}
if (canSetAlertLevel === SWIPE_NEEDED) {
setShowAlertLevelConfirm([alertLevel, alertLevelTick]);
} else {
act("changeSecurityLevel", {
newSecurityLevel: alertLevel,
});
}
}}
/>
);
};
const MessageModal = (props, context) => {
const { data } = useBackend(context);
const { maxMessageLength } = data;
const [input, setInput] = useLocalState(context, props.label, "");
const longEnough = props.minLength === undefined
|| input.length >= props.minLength;
return (
<Modal>
<Flex direction="column">
<Flex.Item fontSize="16px" maxWidth="90vw" mb={1}>
{props.label}:
</Flex.Item>
<Flex.Item mr={2} mb={1}>
<TextArea
fluid
height="20vh"
width="80vw"
backgroundColor="black"
textColor="white"
onInput={(_, value) => {
setInput(value.substring(0, maxMessageLength));
}}
value={input}
/>
</Flex.Item>
<Flex.Item>
<Button
icon={props.icon}
content={props.buttonText}
color="good"
disabled={!longEnough}
tooltip={!longEnough ? "You need a longer reason." : ""}
tooltipPosition="right"
onClick={() => {
if (longEnough) {
setInput("");
props.onSubmit(input);
}
}}
/>
<Button
icon="times"
content="Cancel"
color="bad"
onClick={props.onBack}
/>
</Flex.Item>
{!!props.notice && (
<Flex.Item maxWidth="90vw">{props.notice}</Flex.Item>
)}
</Flex>
</Modal>
);
};
const NoConnectionModal = () => {
return (
<Dimmer>
<Flex direction="column" textAlign="center" width="300px">
<Flex.Item>
<Icon
color="red"
name="wifi"
size={10}
/>
<Blink>
<div
style={{
background: "#db2828",
bottom: "60%",
left: "25%",
height: "10px",
position: "relative",
transform: "rotate(45deg)",
width: "150px",
}}
/>
</Blink>
</Flex.Item>
<Flex.Item fontSize="16px">
A connection to the station cannot be established.
</Flex.Item>
</Flex>
</Dimmer>
);
};
const PageBuyingShuttle = (props, context) => {
const { act, data } = useBackend(context);
return (
<Box>
<Section>
<Button
icon="chevron-left"
content="Back"
onClick={() => act("setState", { state: STATE_MAIN })}
/>
</Section>
<Section>
Budget: <b>{data.budget.toLocaleString()}</b> credits
</Section>
{sortByCreditCost(data.shuttles).map(shuttle => (
<Section
title={(
<span
style={{
display: "inline-block",
width: "70%",
}}>
{shuttle.name}
</span>
)}
key={shuttle.ref}
buttons={(
<Button
content={`${shuttle.creditCost.toLocaleString()} credits`}
disabled={data.budget < shuttle.creditCost}
onClick={() => act("purchaseShuttle", {
shuttle: shuttle.ref,
})}
tooltip={
data.budget < shuttle.creditCost
? `You need ${shuttle.creditCost - data.budget} more credits.`
: undefined
}
tooltipPosition="left"
/>
)}>
<Box>{shuttle.description}</Box>
{
shuttle.prerequisites
? <b>Prerequisites: {shuttle.prerequisites}</b>
: null
}
</Section>
))}
</Box>
);
};
const PageChangingStatus = (props, context) => {
const { act, data } = useBackend(context);
const { maxStatusLineLength } = data;
const [lineOne, setLineOne] = useLocalState(context, "lineOne", data.lineOne);
const [lineTwo, setLineTwo] = useLocalState(context, "lineTwo", data.lineTwo);
return (
<Box>
<Section>
<Button
icon="chevron-left"
content="Back"
onClick={() => act("setState", { state: STATE_MAIN })}
/>
</Section>
<Section>
<Flex direction="column">
<Flex.Item>
<Button
icon="times"
content="Clear Alert"
color="bad"
onClick={() => act("setStatusPicture", { picture: "blank" })}
/>
</Flex.Item>
<Flex.Item mt={1}>
<Button
icon="check-square-o"
content="Default"
onClick={() => act("setStatusPicture", { picture: "default" })}
/>
<Button
icon="bell-o"
content="Red Alert"
onClick={() => act("setStatusPicture", { picture: "redalert" })}
/>
<Button
icon="exclamation-triangle"
content="Lockdown"
onClick={() => act("setStatusPicture", { picture: "lockdown" })}
/>
<Button
icon="exclamation-circle"
content="Biohazard"
onClick={() => act("setStatusPicture", { picture: "biohazard" })}
/>
<Button
icon="space-shuttle"
content="Shuttle ETA"
onClick={() => act("setStatusPicture", { picture: "shuttle" })}
/>
</Flex.Item>
</Flex>
</Section>
<Section title="Message">
<Flex direction="column">
<Flex.Item mb={1}>
<Input
maxLength={maxStatusLineLength}
value={lineOne}
width="200px"
onChange={(_, value) => setLineOne(value)}
/>
</Flex.Item>
<Flex.Item mb={1}>
<Input
maxLength={maxStatusLineLength}
value={lineTwo}
width="200px"
onChange={(_, value) => setLineTwo(value)}
/>
</Flex.Item>
<Flex.Item>
<Button
icon="comment-o"
content="Message"
onClick={() => act("setStatusMessage", {
lineOne,
lineTwo,
})}
/>
</Flex.Item>
</Flex>
</Section>
</Box>
);
};
const PageMain = (props, context) => {
const { act, data } = useBackend(context);
const {
alertLevel,
alertLevelTick,
callShuttleReasonMinLength,
canBuyShuttles,
canMakeAnnouncement,
canMessageAssociates,
canRecallShuttles,
canRequestNuke,
canSendToSectors,
canSetAlertLevel,
canToggleEmergencyAccess,
emagged,
emergencyAccess,
importantActionReady,
sectors,
shuttleCalled,
shuttleCalledPreviously,
shuttleCanEvacOrFailReason,
shuttleLastCalled,
shuttleRecallable,
} = data;
const [callingShuttle, setCallingShuttle] = useLocalState(
context, "calling_shuttle", false);
const [messagingAssociates, setMessagingAssociates] = useLocalState(
context, "messaging_associates", false);
const [messagingSector, setMessagingSector] = useLocalState(
context, "messaing_sector", null);
const [requestingNukeCodes, setRequestingNukeCodes] = useLocalState(
context, "requesting_nuke_codes", false);
const [
[showAlertLevelConfirm, confirmingAlertLevelTick],
setShowAlertLevelConfirm,
] = useLocalState(context, "showConfirmPrompt", [null, null]);
return (
<Box>
<Section title="Emergency Shuttle">
{
shuttleCalled
? <Button.Confirm
icon="space-shuttle"
content="Recall Emergency Shuttle"
color="bad"
disabled={!canRecallShuttles || !shuttleRecallable}
tooltip={(
canRecallShuttles && (
!shuttleRecallable && "It's too late for the emergency shuttle to be recalled."
) || (
"You do not have permission to recall the emergency shuttle."
)
)}
tooltipPosition="bottom-right"
onClick={() => act("recallShuttle")}
/>
: <Button
icon="space-shuttle"
content="Call Emergency Shuttle"
disabled={shuttleCanEvacOrFailReason !== 1}
tooltip={
shuttleCanEvacOrFailReason !== 1
? shuttleCanEvacOrFailReason
: undefined
}
tooltipPosition="bottom-right"
onClick={() => setCallingShuttle(true)}
/>
}
{!!shuttleCalledPreviously && (
shuttleLastCalled && (
<Box>
Most recent shuttle call/recall traced to:
{" "}<b>{shuttleLastCalled}</b>
</Box>
) || (
<Box>Unable to trace most recent shuttle/recall signal.</Box>
)
)}
</Section>
{!!canSetAlertLevel && (
<Section title="Alert Level">
<Flex justify="space-between">
<Flex.Item>
<Box>
Currently on <b>{capitalize(alertLevel)}</b> Alert
</Box>
</Flex.Item>
<Flex.Item>
<AlertButton
alertLevel="green"
showAlertLevelConfirm={showAlertLevelConfirm}
setShowAlertLevelConfirm={setShowAlertLevelConfirm}
/>
<AlertButton
alertLevel="blue"
showAlertLevelConfirm={showAlertLevelConfirm}
setShowAlertLevelConfirm={setShowAlertLevelConfirm}
/>
</Flex.Item>
</Flex>
</Section>
)}
<Section title="Functions">
<Flex
direction="column">
{!!canMakeAnnouncement && <Button
icon="bullhorn"
content="Make Priority Announcement"
onClick={() => act("makePriorityAnnouncement")}
/>}
{!!canToggleEmergencyAccess && <Button.Confirm
icon="id-card-o"
content={`${emergencyAccess ? "Disable" : "Enable"} Emergency Maintenance Access`}
color={emergencyAccess ? "bad" : undefined}
onClick={() => act("toggleEmergencyAccess")}
/>}
<Button
icon="desktop"
content="Set Status Display"
onClick={() => act("setState", { state: STATE_CHANGING_STATUS })}
/>
<Button
icon="envelope-o"
content="Message List"
onClick={() => act("setState", { state: STATE_MESSAGES })}
/>
{(canBuyShuttles !== 0) && <Button
icon="shopping-cart"
content="Purchase Shuttle"
disabled={canBuyShuttles !== 1}
// canBuyShuttles is a string detailing the fail reason
// if one can be given
tooltip={canBuyShuttles !== 1 ? canBuyShuttles : undefined}
tooltipPosition="right"
onClick={() => act("setState", { state: STATE_BUYING_SHUTTLE })}
/>}
{!!canMessageAssociates && <Button
icon="comment-o"
content={`Send message to ${emagged ? "[UNKNOWN]" : "CentCom"}`}
disabled={!importantActionReady}
onClick={() => setMessagingAssociates(true)}
/>}
{!!canRequestNuke && <Button
icon="radiation"
content="Request Nuclear Authentication Codes"
disabled={!importantActionReady}
onClick={() => setRequestingNukeCodes(true)}
/>}
{!!emagged && <Button
icon="undo"
content="Restore Backup Routing Data"
onClick={() => act("restoreBackupRoutingData")}
/>}
</Flex>
</Section>
{!!canMessageAssociates && messagingAssociates && <MessageModal
label={`Message to transmit to ${emagged ? "[ABNORMAL ROUTING COORDINATES]" : "CentCom"} via quantum entanglement`}
notice="Please be aware that this process is very expensive, and abuse will lead to...termination. Transmission does not guarantee a response."
icon="bullhorn"
buttonText="Send"
onBack={() => setMessagingAssociates(false)}
onSubmit={message => {
setMessagingAssociates(false);
act("messageAssociates", {
message,
});
}}
/>}
{!!canRequestNuke && requestingNukeCodes && <MessageModal
label="Reason for requesting nuclear self-destruct codes"
notice="Misuse of the nuclear request system will not be tolerated under any circumstances. Transmission does not guarantee a response."
icon="bomb"
buttonText="Request Codes"
onBack={() => setRequestingNukeCodes(false)}
onSubmit={reason => {
setRequestingNukeCodes(false);
act("requestNukeCodes", {
reason,
});
}}
/>}
{!!callingShuttle && <MessageModal
label="Nature of emergency"
icon="space-shuttle"
buttonText="Call Shuttle"
minLength={callShuttleReasonMinLength}
onBack={() => setCallingShuttle(false)}
onSubmit={reason => {
setCallingShuttle(false);
act("callShuttle", {
reason,
});
}}
/>}
{
!!canSetAlertLevel
&& showAlertLevelConfirm
&& confirmingAlertLevelTick === alertLevelTick
&& (
<Modal>
<Flex
direction="column"
textAlign="center"
width="300px">
<Flex.Item fontSize="16px" mb={2}>
Swipe ID to confirm change
</Flex.Item>
<Flex.Item mr={2} mb={1}>
<Button
icon="id-card-o"
content="Swipe ID"
color="good"
fontSize="16px"
onClick={() => act("changeSecurityLevel", {
newSecurityLevel: showAlertLevelConfirm,
})}
/>
<Button
icon="times"
content="Cancel"
color="bad"
fontSize="16px"
onClick={() => setShowAlertLevelConfirm(false)}
/>
</Flex.Item>
</Flex>
</Modal>
)
}
{
!!canSendToSectors
&& sectors.length > 0
&& (
<Section title="Allied Sectors">
<Flex
direction="column">
{
sectors.map(sectorName => (
<Flex.Item key={sectorName}>
<Button
content={
`Send a message to station in ${sectorName} sector`
}
disabled={!importantActionReady}
onClick={() => setMessagingSector(sectorName)}
/>
</Flex.Item>
))
}
{sectors.length > 2 && (
<Flex.Item>
<Button
content="Send a message to all allied stations"
disabled={!importantActionReady}
onClick={() => setMessagingSector("all")}
/>
</Flex.Item>
)}
</Flex>
</Section>
)
}
{
!!canSendToSectors
&& sectors.length > 0
&& messagingSector
&& <MessageModal
label="Message to send to allied station"
notice="Please be aware that this process is very expensive, and abuse will lead to...termination."
icon="bullhorn"
buttonText="Send"
onBack={() => setMessagingSector(null)}
onSubmit={message => {
act("sendToOtherSector", {
destination: messagingSector,
message,
});
setMessagingSector(null);
}}
/>
}
</Box>
);
};
const PageMessages = (props, context) => {
const { act, data } = useBackend(context);
const messages = data.messages || [];
const children = [];
children.push((
<Section>
<Button
icon="chevron-left"
content="Back"
onClick={() => act("setState", { state: STATE_MAIN })}
/>
</Section>
));
const messageElements = [];
for (const [messageIndex, message] of Object.entries(messages)) {
let answers = null;
if (message.possibleAnswers.length > 0) {
answers = (
<Box mt={1}>
{message.possibleAnswers.map((answer, answerIndex) => (
<Button
content={answer}
color={message.answered === answerIndex + 1 ? "good" : undefined}
key={answerIndex}
onClick={message.answered ? undefined : () => act("answerMessage", {
message: messageIndex + 1,
answer: answerIndex + 1,
})}
/>
))}
</Box>
);
}
const textHtml = {
__html: sanitizeText(message.content),
};
messageElements.push((
<Section
title={message.title}
key={messageIndex}
buttons={(
<Button.Confirm
icon="trash"
content="Delete"
color="red"
onClick={() => act("deleteMessage", {
message: messageIndex + 1,
})}
/>
)}>
<Box
dangerouslySetInnerHTML={textHtml} />
{answers}
</Section>
));
}
children.push(messageElements.reverse());
return children;
};
export const CommunicationsConsole = (props, context) => {
const { act, data } = useBackend(context);
const {
authenticated,
authorizeName,
canLogOut,
emagged,
hasConnection,
page,
} = data;
return (
<Window
width={400}
height={650}
theme={emagged ? "syndicate" : undefined}
resizable>
<Window.Content scrollable>
{!hasConnection && <NoConnectionModal />}
{(canLogOut || !authenticated)
? (
<Section title="Authentication">
<Button
icon={authenticated ? "sign-out-alt" : "sign-in-alt"}
content={authenticated ? `Log Out${authorizeName ? ` (${authorizeName})` : ""}` : "Log In"}
color={authenticated ? "bad" : "good"}
onClick={() => act("toggleAuthentication")}
/>
</Section>
)
: null}
{!!authenticated && (
page === STATE_BUYING_SHUTTLE && <PageBuyingShuttle />
|| page === STATE_CHANGING_STATUS && <PageChangingStatus />
|| page === STATE_MAIN && <PageMain />
|| page === STATE_MESSAGES && <PageMessages />
|| <Box>Page not implemented: {page}</Box>
)}
</Window.Content>
</Window>
);
};
+70 -45
View File
@@ -1,3 +1,4 @@
import { sortBy } from 'common/collections';
import { useBackend } from '../backend';
import { Box, Button, ColorBox, Section, Table } from '../components';
import { COLORS } from '../constants';
@@ -62,7 +63,7 @@ export const CrewConsole = () => {
return (
<Window
title="Crew Monitor"
width={800}
width={600}
height={600}
resizable>
<Window.Content scrollable>
@@ -76,7 +77,9 @@ export const CrewConsole = () => {
const CrewTable = (props, context) => {
const { act, data } = useBackend(context);
const sensors = data.sensors || [];
const sensors = sortBy(
s => s.ijob
)(data.sensors ?? []);
return (
<Table>
<Table.Row>
@@ -97,50 +100,72 @@ const CrewTable = (props, context) => {
)}
</Table.Row>
{sensors.map(sensor => (
<Table.Row key={sensor.name}>
<Table.Cell
bold={jobIsHead(sensor.ijob)}
color={jobToColor(sensor.ijob)}>
{sensor.name} ({sensor.assignment})
</Table.Cell>
<Table.Cell collapsing textAlign="center">
<ColorBox
color={healthToColor(
sensor.oxydam,
sensor.toxdam,
sensor.burndam,
sensor.brutedam)} />
</Table.Cell>
<Table.Cell collapsing textAlign="center">
{sensor.oxydam !== null ? (
<Box inline>
<HealthStat type="oxy" value={sensor.oxydam} />
{'/'}
<HealthStat type="toxin" value={sensor.toxdam} />
{'/'}
<HealthStat type="burn" value={sensor.burndam} />
{'/'}
<HealthStat type="brute" value={sensor.brutedam} />
</Box>
) : (
sensor.life_status ? 'Alive' : 'Dead'
)}
</Table.Cell>
<Table.Cell>
{sensor.pos_x !== null ? sensor.area : 'N/A'}
</Table.Cell>
{!!data.link_allowed && (
<Table.Cell collapsing>
<Button
content="Track"
disabled={!sensor.can_track}
onClick={() => act('select_person', {
name: sensor.name,
})} />
</Table.Cell>
)}
</Table.Row>
<CrewTableEntry sensor_data={sensor} key={sensor.ref} />
))}
</Table>
);
};
const CrewTableEntry = (props, context) => {
const { act, data } = useBackend(context);
const { link_allowed } = data;
const { sensor_data } = props;
const {
name,
assignment,
ijob,
life_status,
oxydam,
toxdam,
burndam,
brutedam,
area,
can_track,
} = sensor_data;
return (
<Table.Row>
<Table.Cell
bold={jobIsHead(ijob)}
color={jobToColor(ijob)}>
{name}{assignment !== undefined ? ` (${assignment})` : ""}
</Table.Cell>
<Table.Cell collapsing textAlign="center">
<ColorBox
color={healthToColor(
oxydam,
toxdam,
burndam,
brutedam)} />
</Table.Cell>
<Table.Cell collapsing textAlign="center">
{oxydam !== undefined ? (
<Box inline>
<HealthStat type="oxy" value={oxydam} />
{'/'}
<HealthStat type="toxin" value={toxdam} />
{'/'}
<HealthStat type="burn" value={burndam} />
{'/'}
<HealthStat type="brute" value={brutedam} />
</Box>
) : (
life_status ? 'Alive' : 'Dead'
)}
</Table.Cell>
<Table.Cell>
{area !== undefined ? area : 'N/A'}
</Table.Cell>
{!!link_allowed && (
<Table.Cell collapsing>
<Button
content="Track"
disabled={!can_track}
onClick={() => act('select_person', {
name: name,
})} />
</Table.Cell>
)}
</Table.Row>
);
};
@@ -0,0 +1,146 @@
import { multiline } from 'common/string';
import { useBackend } from '../backend';
import { Button, Input, LabeledList, Section } from '../components';
import { Window } from '../layouts';
const TOOLTIP_NAME = multiline`
Enter a new name for this unit. Set to blank to reset to default,
which means unit will be able to choose it's own name.
`;
const TOOLTIP_LOCOMOTION = multiline`
If restricted, unit will be
under lockdown until released.
`;
const TOOLTIP_PANEL = multiline`
If unlocked, unit's cover panel will be
accessible even without proper authorization.
`;
const TOOLTIP_AISYNC = multiline`
If closed, this unit will
not be paired with any AI.
`;
const TOOLTIP_AI = multiline`
Controls who will be the
master AI of this unit.
`;
const TOOLTIP_LAWSYNC = multiline`
If closed, this unit will not synchronize
it's laws with it's master AI.
`;
export const CyborgBootDebug = (props, context) => {
const { act, data } = useBackend(context);
const {
designation,
master,
lawsync,
aisync,
locomotion,
panel,
} = data;
return (
<Window
width={master?.length > 26 ? 537 : 440}
height={289}>
<Window.Content>
<Section title="Basic Settings">
<LabeledList>
<LabeledList.Item
label="Designation"
buttons={(
<Button
icon="info"
tooltip={TOOLTIP_NAME}
tooltipPosition="left" />
)}>
<Input
fluid
value={designation || "Default Cyborg"}
onChange={(e, value) => act('rename', {
new_name: value,
})} />
</LabeledList.Item>
<LabeledList.Item
label="Servo Motor Functions"
buttons={(
<Button
icon="info"
tooltip={TOOLTIP_LOCOMOTION}
tooltipPosition="left" />
)}>
<Button
icon={locomotion ? "unlock" : "lock"}
content={locomotion ? "Free" : "Restricted"}
color={locomotion ? "good" : "bad"}
onClick={() => act('locomotion')} />
</LabeledList.Item>
<LabeledList.Item
label="Cover Panel"
buttons={(
<Button
icon="info"
tooltip={TOOLTIP_PANEL}
tooltipPosition="left" />
)}>
<Button
icon={panel ? "lock" : "unlock"}
content={panel ? "Locked" : "Unlocked"}
onClick={() => act('panel')} />
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="AI Settings">
<LabeledList>
<LabeledList.Item
label="AI Connection Port"
buttons={(
<Button
icon="info"
tooltip={TOOLTIP_AISYNC}
tooltipPosition="left" />
)}>
<Button
icon={aisync ? "unlock" : "lock"}
content={aisync ? "Open" : "Closed"}
onClick={() => act('aisync')} />
</LabeledList.Item>
<LabeledList.Item
label="Master AI"
buttons={(
<Button
icon="info"
tooltip={TOOLTIP_AI}
tooltipPosition="left" />
)}>
<Button
icon={!aisync ? "times" : master ? "edit" : "sync"}
content={!aisync ? "None" : master || "Automatic"}
color={master ? "default" : "good"}
disabled={!aisync}
onClick={() => act('set_ai')} />
</LabeledList.Item>
<LabeledList.Item
label="LawSync Port"
buttons={(
<Button
icon="info"
tooltip={TOOLTIP_LAWSYNC}
tooltipPosition="top-left" />
)}>
<Button
icon={!aisync ? "lock" : lawsync ? "unlock" : "lock"}
content={!aisync ? "Closed" : lawsync ? "Open" : "Closed"}
disabled={!aisync}
onClick={() => act('lawsync')} />
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
+1 -1
View File
@@ -37,7 +37,7 @@ export const DnaVault = (props, context) => {
</LabeledList.Item>
<LabeledList.Item label="Animal DNA">
<ProgressBar
value={animals / animals}>
value={animals / animals_max}>
{animals + ' / ' + animals_max + ' Samples'}
</ProgressBar>
</LabeledList.Item>
@@ -28,7 +28,7 @@ export const ExosuitControlConsole = (props, context) => {
<Fragment>
<Button
icon="envelope"
content="Send Message"
content="Message"
disabled={!mech.pilot}
onClick={() => act('send_message', {
tracker_ref: mech.tracker_ref,
@@ -76,7 +76,14 @@ export const ExosuitControlConsole = (props, context) => {
) || 'Not Equipped'}
</LabeledList.Item>
<LabeledList.Item label="Pilot">
{mech.pilot || 'None'}
{mech.pilot.length > 0 && (
mech.pilot.map(pilot => (
<Box
key={pilot}
inline>
{pilot}{mech.pilot.length > 1 ? '|' : ''}
</Box>
))) || 'None'}
</LabeledList.Item>
<LabeledList.Item label="Location">
{mech.location || 'Unknown'}
@@ -1,10 +1,11 @@
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 { classes } from 'common/react';
import { createSearch } from 'common/string';
import { Fragment } from 'inferno';
import { useBackend, useSharedState } from '../backend';
import { Box, Button, Flex, Icon, Input, NumberInput, ProgressBar, Section, Tabs } from '../components';
import { formatMoney, formatSiUnit } from '../format';
import { Window } from '../layouts';
const MATERIAL_KEYS = {
"iron": "sheet-metal_3",
@@ -0,0 +1,307 @@
import { useBackend, useLocalState } from "../backend";
import { Fragment } from 'inferno';
import { Box, Button, Collapsible, ColorBox, Dropdown, Input, LabeledList, NoticeBox, NumberInput, Section } from '../components';
import { Window } from '../layouts';
import { map } from 'common/collections';
import { toFixed } from 'common/math';
import { numberOfDecimalDigits } from "../../common/math";
const FilterIntegerEntry = (props, context) => {
const { value, name, filterName } = props;
const { act } = useBackend(context);
return (
<NumberInput
value={value}
minValue={-500}
maxValue={500}
stepPixelSize={5}
width="39px"
onDrag={(e, value) => act('modify_filter_value', {
name: filterName,
new_data: {
[name]: value,
},
})} />
);
};
const FilterFloatEntry = (props, context) => {
const { value, name, filterName } = props;
const { act } = useBackend(context);
const [step, setStep] = useLocalState(context, `${filterName}-${name}`, 0.01);
return (
<Fragment>
<NumberInput
value={value}
minValue={-500}
maxValue={500}
stepPixelSize={4}
step={step}
format={value => toFixed(value, numberOfDecimalDigits(step))}
width="80px"
onDrag={(e, value) => act('transition_filter_value', {
name: filterName,
new_data: {
[name]: value,
},
})} />
<Box
inline
ml={2}
mr={1}>
Step:
</Box>
<NumberInput
value={step}
step={0.001}
format={value => toFixed(value, 4)}
width="70px"
onChange={(e, value) => setStep(value)} />
</Fragment>
);
};
const FilterTextEntry = (props, context) => {
const { value, name, filterName } = props;
const { act } = useBackend(context);
return (
<Input
value={value}
width="250px"
onInput={(e, value) => act('modify_filter_value', {
name: filterName,
new_data: {
[name]: value,
},
})} />
);
};
const FilterColorEntry = (props, context) => {
const { value, filterName, name } = props;
const { act } = useBackend(context);
return (
<Fragment>
<Button
icon="pencil-alt"
onClick={() => act('modify_color_value', {
name: filterName,
})} />
<ColorBox
color={value}
mr={0.5} />
<Input
value={value}
width="90px"
onInput={(e, value) => act('transition_filter_value', {
name: filterName,
new_data: {
[name]: value,
},
})} />
</Fragment>
);
};
const FilterIconEntry = (props, context) => {
const { value, filterName } = props;
const { act } = useBackend(context);
return (
<Fragment>
<Button
icon="pencil-alt"
onClick={() => act('modify_icon_value', {
name: filterName,
})} />
<Box inline ml={1}>
{value}
</Box>
</Fragment>
);
};
const FilterFlagsEntry = (props, context) => {
const { name, value, filterName, filterType } = props;
const { act, data } = useBackend(context);
const filterInfo = data.filter_info;
const flags = filterInfo[filterType]['flags'];
return (
map((bitField, flagName) => (
<Button.Checkbox
checked={value & bitField}
content={flagName}
onClick={() => act('modify_filter_value', {
name: filterName,
new_data: {
[name]: value ^ bitField,
},
})} />
))(flags)
);
};
const FilterDataEntry = (props, context) => {
const { name, value, hasValue, filterName } = props;
const filterEntryTypes = {
int: <FilterIntegerEntry {...props} />,
float: <FilterFloatEntry {...props} />,
string: <FilterTextEntry {...props} />,
color: <FilterColorEntry {...props} />,
icon: <FilterIconEntry {...props} />,
flags: <FilterFlagsEntry {...props} />,
};
const filterEntryMap = {
x: 'float',
y: 'float',
icon: 'icon',
render_source: 'string',
flags: 'flags',
size: 'float',
color: 'color',
offset: 'float',
radius: 'float',
falloff: 'float',
density: 'int',
threshold: 'float',
factor: 'float',
repeat: 'int',
};
return (
<LabeledList.Item label={name}>
{filterEntryTypes[filterEntryMap[name]] || "Not Found (This is an error)"}
{' '}
{!hasValue && <Box inline color="average">(Default)</Box>}
</LabeledList.Item>
);
};
const FilterEntry = (props, context) => {
const { act, data } = useBackend(context);
const { name, filterDataEntry } = props;
const { type, priority, ...restOfProps } = filterDataEntry;
const filterDefaults = data["filter_info"];
const targetFilterPossibleKeys = Object.keys(filterDefaults[type]['defaults']);
return (
<Collapsible
title={name + " (" + type + ")"}
buttons={(
<Fragment>
<NumberInput
value={priority}
stepPixelSize={10}
width="60px"
onChange={(e, value) => act('change_priority', {
name: name,
new_priority: value,
})}
/>
<Button.Input
content="Rename"
placeholder={name}
onCommit={(e, new_name) => act('rename_filter', {
name: name,
new_name: new_name,
})}
width="90px" />
<Button.Confirm
icon="minus"
onClick={() => act("remove_filter", { name: name })} />
</Fragment>
)}>
<Section level={2}>
<LabeledList>
{targetFilterPossibleKeys.map(entryName => {
const defaults = filterDefaults[type]['defaults'];
const value = restOfProps[entryName] || defaults[entryName];
const hasValue = value !== defaults[entryName];
return (
<FilterDataEntry
key={entryName}
filterName={name}
filterType={type}
name={entryName}
value={value}
hasValue={hasValue} />
);
})}
</LabeledList>
</Section>
</Collapsible>
);
};
export const Filteriffic = (props, context) => {
const { act, data } = useBackend(context);
const name = data.target_name || "Unknown Object";
const filters = data.target_filter_data || {};
const hasFilters = filters !== {};
const filterDefaults = data["filter_info"];
const [massApplyPath, setMassApplyPath] = useLocalState(context, 'massApplyPath', '');
const [hiddenSecret, setHiddenSecret] = useLocalState(context, 'hidden', false);
return (
<Window
width={500}
height={500}
title="Filteriffic"
resizable>
<Window.Content scrollable>
<NoticeBox danger>
DO NOT MESS WITH EXISTING FILTERS IF YOU DO NOT KNOW THE CONSEQUENCES.
YOU HAVE BEEN WARNED.
</NoticeBox>
<Section
title={hiddenSecret ? (
<Fragment>
<Box mr={0.5} inline>
MASS EDIT:
</Box>
<Input
value={massApplyPath}
width="100px"
onInput={(e, value) => setMassApplyPath(value)} />
<Button.Confirm
content="Apply"
confirmContent="ARE YOU SURE?"
onClick={() => act('mass_apply', { path: massApplyPath })} />
</Fragment>
) : (
<Box
inline
onDblClick={() => setHiddenSecret(true)}>
{name}
</Box>
)}
buttons={(
<Dropdown
icon="plus"
displayText="Add Filter"
nochevron
options={Object.keys(filterDefaults)}
onSelected={value => act('add_filter', {
name: 'default',
priority: 10,
type: value,
})} />
)} >
{!hasFilters ? (
<Box>
No filters
</Box>
) : (
map((entry, key) => (
<FilterEntry filterDataEntry={entry} name={key} key={key} />
))(filters)
)}
</Section>
</Window.Content>
</Window>
);
};
@@ -1,5 +1,6 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Button, Flex, Fragment, Section, NoticeBox } from '../components';
import { Button, Flex, NoticeBox, Section } from '../components';
import { Window } from '../layouts';
export const GhostPoolProtection = (props, context) => {
+2 -2
View File
@@ -6,11 +6,11 @@ export const Holodeck = (props, context) => {
const { act, data } = useBackend(context);
const {
can_toggle_safety,
default_programs = [],
emag_programs = [],
emagged,
program,
} = data;
const default_programs = data.default_programs || [];
const emag_programs = data.emag_programs || [];
return (
<Window
width={400}
+2 -2
View File
@@ -68,8 +68,8 @@ const HolopadContent = (props, context) => {
<Button
icon="bell"
content={on_cooldown
? "AI Presence Requested"
: "Request AI Presence"}
? "AI's Presence Requested"
: "Request AI's Presence"}
disabled={!on_network || on_cooldown}
onClick={() => act('AIrequest')} />
)} >
@@ -0,0 +1,106 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, Section, LabeledList, NumberInput } from '../components';
import { Window } from '../layouts';
export const MassDriverControl = (props, context) => {
const { act, data } = useBackend(context);
const {
connected,
minutes,
seconds,
timing,
power,
poddoor,
} = data;
return (
<Window
width={300}
height={connected ? 215 : 107}>
<Window.Content>
<Fragment>
{!!connected && (
<Section
title="Auto Launch"
buttons={(
<Button
icon={"clock-o"}
content={timing ? 'Stop' : 'Start'}
selected={timing}
onClick={() => act('time')} />
)}>
<Button
icon="fast-backward"
disabled={timing}
onClick={() => act('input', { adjust: -30 })} />
<Button
icon="backward"
disabled={timing}
onClick={() => act('input', { adjust: -1 })} />
{' '}
{String(minutes).padStart(2, '0')}:
{String(seconds).padStart(2, '0')}
{' '}
<Button
icon="forward"
disabled={timing}
onClick={() => act('input', { adjust: 1 })} />
<Button
icon="fast-forward"
disabled={timing}
onClick={() => act('input', { adjust: 30 })} />
</Section>
)}
<Section
title="Controls"
buttons={(
<Button
icon={"toggle-on"}
content="Toggle Outer Door"
disabled={timing || !poddoor}
onClick={() => act('door')} />
)}>
{!!connected && (
<Fragment>
<LabeledList>
<LabeledList.Item
label="Power Level"
buttons={
<Button
icon={"bomb"}
content="Test Fire"
disabled={timing}
onClick={() => act('driver_test')} />
}>
<NumberInput
value={power}
width="40px"
minValue={0.25}
maxValue={16}
onChange={(e, value) => {
return act('set_power', {
power: value,
});
}} />
</LabeledList.Item>
</LabeledList>
<Button
fluid
content="Launch"
disabled={timing}
mt={1.5}
icon="arrow-up"
textAlign="center"
onClick={() => act('launch')} />
</Fragment>
) || (
<Box color="bad">
No connected mass driver
</Box>
)}
</Section>
</Fragment>
</Window.Content>
</Window>
);
};
+3
View File
@@ -1,3 +1,6 @@
/**
* THIS FILE WILL SOON BE DEPRICATED!
*/
import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
@@ -0,0 +1,15 @@
import { CargoContent } from './Cargo.js';
import { NtosWindow } from '../layouts';
export const NtosCargo = (props, context) => {
return (
<NtosWindow
width={800}
height={500}
resizable>
<NtosWindow.Content scrollable>
<CargoContent />
</NtosWindow.Content>
</NtosWindow>
);
};
@@ -24,7 +24,8 @@ export const NtosFileManager = (props, context) => {
name: file,
new_name: newName,
})}
onDuplicate={file => act('PRG_clone', { file: file })} />
onDuplicate={file => act('PRG_clone', { file: file })}
onToggleSilence={file => act('PRG_togglesilence', { name: file })} />
</Section>
{usbconnected && (
<Section title="Data Disk">
@@ -54,6 +55,7 @@ const FileTable = props => {
onUpload,
onDelete,
onRename,
onToggleSilence,
} = props;
return (
<Table>
@@ -89,6 +91,13 @@ const FileTable = props => {
{file.size}
</Table.Cell>
<Table.Cell collapsing>
{!!file.alert_able && (
<Button
icon={file.alert_silenced ? 'bell-slash' : 'bell'}
color={file.alert_silenced ? 'red' : 'default'}
tooltip={file.alert_silenced ? 'Unmute Alerts' : 'Mute Alerts'}
onClick={() => onToggleSilence(file.name)} />
)}
{!file.undeletable && (
<Fragment>
<Button.Confirm
+24 -40
View File
@@ -2,24 +2,6 @@ import { useBackend } from '../backend';
import { Button, ColorBox, Section, Table } from '../components';
import { NtosWindow } from '../layouts';
const PROGRAM_ICONS = {
compconfig: 'cog',
ntndownloader: 'download',
filemanager: 'folder',
smmonitor: 'radiation',
alarmmonitor: 'bell',
cardmod: 'id-card',
arcade: 'gamepad',
ntnrc_client: 'comment-alt',
nttransfer: 'exchange-alt',
powermonitor: 'plug',
job_manage: 'address-book',
crewmani: 'clipboard-list',
robocontrol: 'robot',
atmosscan: 'thermometer-half',
shipping: 'tags',
};
export const NtosMain = (props, context) => {
const { act, data } = useBackend(context);
const {
@@ -29,6 +11,7 @@ export const NtosMain = (props, context) => {
light_on,
comp_light_color,
removable_media = [],
cardholder,
login = [],
} = data;
return (
@@ -58,25 +41,27 @@ export const NtosMain = (props, context) => {
</Button>
</Section>
)}
<Section
title="User Login"
buttons={(
<Button
icon="eject"
content="Eject ID"
disabled={!login.IDName}
onClick={() => act('PC_Eject_Disk', { name: "ID" })}
/>
)}>
<Table>
<Table.Row>
ID Name: {login.IDName}
</Table.Row>
<Table.Row>
Assignment: {login.IDJob}
</Table.Row>
</Table>
</Section>
{!!cardholder && (
<Section
title="User Login"
buttons={(
<Button
icon="eject"
content="Eject ID"
disabled={!login.IDName}
onClick={() => act('PC_Eject_Disk', { name: "ID" })}
/>
)}>
<Table>
<Table.Row>
ID Name: {login.IDName}
</Table.Row>
<Table.Row>
Assignment: {login.IDJob}
</Table.Row>
</Table>
</Section>
)}
{!!removable_media.length && (
<Section title="Media Eject">
<Table>
@@ -103,9 +88,8 @@ export const NtosMain = (props, context) => {
<Table.Cell>
<Button
fluid
color="transparent"
icon={PROGRAM_ICONS[program.name]
|| 'window-maximize-o'}
color={program.alert ? 'yellow' : 'transparent'}
icon={program.icon}
content={program.desc}
onClick={() => act('PC_runprogram', {
name: program.name,
@@ -43,11 +43,20 @@ export const NtosNetDownloader = (props, context) => {
</LabeledList>
</Section>
<Section>
{downloadable_programs.map(program => (
<Program
key={program.filename}
program={program} />
))}
{downloadable_programs
.filter(program => program.access)
.map(program => (
<Program
key={program.filename}
program={program} />
))}
{downloadable_programs
.filter(program => !program.access)
.map(program => (
<Program
key={program.filename}
program={program} />
))}
</Section>
{!!hackedavailable && (
<Section title="UNKNOWN Software Repository">
@@ -100,7 +109,9 @@ const Program = (props, context) => {
fluid
icon="download"
content="Download"
disabled={downloading || program.size > disk_free}
disabled={
downloading || program.size > disk_free || !program.access
}
onClick={() => act('PRG_downloadfile', {
filename: program.filename,
})} />
@@ -113,6 +124,12 @@ const Program = (props, context) => {
Incompatible!
</Box>
)}
{!(program.access) && (
<Box mt={1} italic fontSize="12px" position="relative">
<Icon mx={1} color="red" name="times" />
Invalid credentials loaded!
</Box>
)}
{program.size > disk_free && (
<Box mt={1} italic fontSize="12px" position="relative">
<Icon mx={1} color="red" name="times" />
@@ -0,0 +1,312 @@
import { Fragment } from 'inferno';
import { useBackend, useSharedState } from '../backend';
import { AnimatedNumber, Box, Button, Flex, LabeledList, ProgressBar, Section, Slider, Tabs } from '../components';
import { NtosWindow } from '../layouts';
export const NtosRobotact = (props, context) => {
const { act, data } = useBackend(context);
const { PC_device_theme } = data;
return (
<NtosWindow
width={800}
height={600}
theme={PC_device_theme}>
<NtosWindow.Content>
<NtosRobotactContent />
</NtosWindow.Content>
</NtosWindow>
);
};
export const NtosRobotactContent = (props, context) => {
const { act, data } = useBackend(context);
const [tab_main, setTab_main] = useSharedState(context, 'tab_main', 1);
const [tab_sub, setTab_sub] = useSharedState(context, 'tab_sub', 1);
const {
charge,
maxcharge,
integrity,
lampIntensity,
cover,
locomotion,
wireModule,
wireCamera,
wireAI,
wireLaw,
sensors,
printerPictures,
printerToner,
printerTonerMax,
thrustersInstalled,
thrustersStatus,
} = data;
const borgName = data.name || [];
const borgType = data.designation || [];
const masterAI = data.masterAI || [];
const laws = data.Laws || [];
const borgLog = data.borgLog || [];
const borgUpgrades = data.borgUpgrades || [];
return (
<Flex
direction={"column"}>
<Flex.Item
position="relative"
mb={1}>
<Tabs>
<Tabs.Tab
icon="list"
lineHeight="23px"
selected={tab_main === 1}
onClick={() => setTab_main(1)}>
Status
</Tabs.Tab>
<Tabs.Tab
icon="list"
lineHeight="23px"
selected={tab_main === 2}
onClick={() => setTab_main(2)}>
Logs
</Tabs.Tab>
</Tabs>
</Flex.Item>
{tab_main === 1 && (
<Fragment>
<Flex
direction={"row"}>
<Flex.Item
width="30%">
<Section
title="Configuration"
fill>
<LabeledList>
<LabeledList.Item
label="Unit">
{borgName.slice(0, 17)}
</LabeledList.Item>
<LabeledList.Item
label="Type">
{borgType}
</LabeledList.Item>
<LabeledList.Item
label="AI">
{masterAI.slice(0, 17)}
</LabeledList.Item>
</LabeledList>
</Section>
</Flex.Item>
<Flex.Item
grow={1}
ml={1}>
<Section
title="Status">
Charge:
<Button
content="Power Alert"
disabled={charge}
onClick={() => act('alertPower')} />
<ProgressBar
value={charge / maxcharge}
ranges={{
good: [0.5, Infinity],
average: [0.1, 0.5],
bad: [-Infinity, 0.1],
}}>
<AnimatedNumber value={charge} />
</ProgressBar>
Chassis Integrity:
<ProgressBar
value={integrity}
minValue={0}
maxValue={100}
ranges={{
bad: [-Infinity, 25],
average: [25, 75],
good: [75, Infinity],
}} />
</Section>
<Section
title="Lamp Power">
<Slider
value={lampIntensity}
step={1}
stepPixelSize={25}
maxValue={5}
minValue={1}
onChange={(e, value) => act('lampIntensity', {
ref: value,
})} />
Lamp power usage: {lampIntensity/2} watts
</Section>
</Flex.Item>
<Flex.Item
width="50%"
ml={1}>
<Section
fitted>
<Tabs
fluid={1}
textAlign="center">
<Tabs.Tab
icon=""
lineHeight="23px"
selected={tab_sub === 1}
onClick={() => setTab_sub(1)}>
Actions
</Tabs.Tab>
<Tabs.Tab
icon=""
lineHeight="23px"
selected={tab_sub === 2}
onClick={() => setTab_sub(2)}>
Upgrades
</Tabs.Tab>
<Tabs.Tab
icon=""
lineHeight="23px"
selected={tab_sub === 3}
onClick={() => setTab_sub(3)}>
Diagnostics
</Tabs.Tab>
</Tabs>
</Section>
{tab_sub === 1 && (
<Section>
<LabeledList>
<LabeledList.Item
label="Maintenance Cover">
<Button.Confirm
content="Unlock"
disabled={cover==="UNLOCKED"}
onClick={() => act('coverunlock')} />
</LabeledList.Item>
<LabeledList.Item
label="Sensor Overlay">
<Button
content={sensors}
onClick={() => act('toggleSensors')} />
</LabeledList.Item>
<LabeledList.Item
label={"Stored Photos (" + printerPictures + ")"}>
<Button
content="View"
disabled={!printerPictures}
onClick={() => act('viewImage')} />
<Button
content="Print"
disabled={!printerPictures}
onClick={() => act('printImage')} />
</LabeledList.Item>
<LabeledList.Item
label="Printer Toner">
<ProgressBar
value={printerToner / printerTonerMax} />
</LabeledList.Item>
{!!thrustersInstalled && (
<LabeledList.Item
label="Toggle Thrusters">
<Button
content={thrustersStatus}
onClick={() => act('toggleThrusters')} />
</LabeledList.Item>
)}
</LabeledList>
</Section>
)}
{tab_sub === 2 && (
<Section>
{borgUpgrades.map(upgrade => (
<Box
mb={1}
key={upgrade}>
{upgrade}
</Box>
))}
</Section>
)}
{tab_sub === 3 && (
<Section>
<LabeledList>
<LabeledList.Item
label="AI Connection"
color={wireAI==="FAULT"?'red': wireAI==="READY"?'yellow': 'green'}>
{wireAI}
</LabeledList.Item>
<LabeledList.Item
label="LawSync"
color={wireLaw==="FAULT"?"red":"green"}>
{wireLaw}
</LabeledList.Item>
<LabeledList.Item
label="Camera"
color={wireCamera==="FAULT"?'red': wireCamera==="DISABLED"?'yellow': 'green'}>
{wireCamera}
</LabeledList.Item>
<LabeledList.Item
label="Module Controller"
color={wireModule==="FAULT"?"red":"green"}>
{wireModule}
</LabeledList.Item>
<LabeledList.Item
label="Motor Controller"
color={locomotion==="FAULT"?'red': locomotion==="DISABLED"?'yellow': 'green'}>
{locomotion}
</LabeledList.Item>
<LabeledList.Item
label="Maintenance Cover"
color={cover==="UNLOCKED"?"red":"green"}>
{cover}
</LabeledList.Item>
</LabeledList>
</Section>
)}
</Flex.Item>
</Flex>
<Flex.Item
height={21}
mt={1}>
<Section
title="Laws"
fill
scrollable
buttons={(
<Fragment>
<Button
content="State Laws"
onClick={() => act('lawstate')} />
<Button
icon="volume-off"
onClick={() => act('lawchannel')} />
</Fragment>
)}>
{laws.map(law => (
<Box
mb={1}
key={law}>
{law}
</Box>
))}
</Section>
</Flex.Item>
</Fragment>
)}
{tab_main === 2 && (
<Flex.Item>
<Section
backgroundColor="black"
height={40}>
<NtosWindow.Content scrollable>
{borgLog.map(log => (
<Box
mb={1}
key={log}>
<font color="green">{log}</font>
</Box>
))}
</NtosWindow.Content>
</Section>
</Flex.Item>
)}
</Flex>
);
};
@@ -0,0 +1,54 @@
import { useBackend } from '../backend';
import { Button, ByondUi } from '../components';
import { NtosWindow } from '../layouts';
import { prevNextCamera, selectCameras, CameraConsoleContent } from './CameraConsole';
export const NtosSecurEye = (props, context) => {
const { act, data, config } = useBackend(context);
const { PC_device_theme, mapRef, activeCamera } = data;
const cameras = selectCameras(data.cameras);
const [
prevCameraName,
nextCameraName,
] = prevNextCamera(cameras, activeCamera);
return (
<NtosWindow
width={800}
height={600}
theme={PC_device_theme}>
<NtosWindow.Content>
<div className="CameraConsole__left">
<CameraConsoleContent />
</div>
<div className="CameraConsole__right">
<div className="CameraConsole__toolbar">
<b>Camera: </b>
{activeCamera
&& activeCamera.name
|| '—'}
</div>
<div className="CameraConsole__toolbarRight">
<Button
icon="chevron-left"
disabled={!prevCameraName}
onClick={() => act('switch_camera', {
name: prevCameraName,
})} />
<Button
icon="chevron-right"
disabled={!nextCameraName}
onClick={() => act('switch_camera', {
name: nextCameraName,
})} />
</div>
<ByondUi
className="CameraConsole__map"
params={{
id: mapRef,
type: 'map',
}} />
</div>
</NtosWindow.Content>
</NtosWindow>
);
};
+37 -9
View File
@@ -1,10 +1,10 @@
import { createSearch } from 'common/string';
import { multiline } from 'common/string';
import { resolveAsset } from '../assets';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Flex, Icon, Input, Section } from '../components';
import { Box, Button, Divider, Flex, Icon, Input, Section } from '../components';
import { Window } from '../layouts';
const PATTERN_DESCRIPTOR = / \[(?:ghost|dead)\]$/;
const PATTERN_NUMBER = / \(([0-9]+)\)$/;
const searchFor = searchText => createSearch(searchText, thing => thing.name);
@@ -43,7 +43,7 @@ const BasicSection = (props, context) => {
{things.map(thing => (
<Button
key={thing.name}
content={thing.name.replace(PATTERN_DESCRIPTOR, "")}
content={thing.name}
onClick={() => act("orbit", {
ref: thing.ref,
})} />
@@ -82,6 +82,7 @@ export const Orbit = (props, context) => {
const {
alive,
antagonists,
auto_observe,
dead,
ghosts,
misc,
@@ -140,6 +141,27 @@ export const Orbit = (props, context) => {
onInput={(_, value) => setSearchText(value)}
onEnter={(_, value) => orbitMostRelevant(value)} />
</Flex.Item>
<Flex.Item>
<Divider vertical />
</Flex.Item>
<Flex.Item>
<Button
inline
color="transparent"
tooltip={multiline`Toggle Auto-Observe. When active, you'll
see the UI / full inventory of whoever you're orbiting. Neat!`}
tooltipPosition="bottom-left"
selected={auto_observe}
icon={auto_observe ? "toggle-on" : "toggle-off"}
onClick={() => act("toggle_observe")} />
<Button
inline
color="transparent"
tooltip="Refresh"
tooltipPosition="bottom-left"
icon="sync-alt"
onClick={() => act("refresh")} />
</Flex.Item>
</Flex>
</Section>
{antagonists.length > 0 && (
@@ -161,7 +183,7 @@ export const Orbit = (props, context) => {
</Section>
)}
<Section title="Alive">
<Section title={`Alive - (${alive.length})`}>
{alive
.filter(searchFor(searchText))
.sort(compareNumberedText)
@@ -173,11 +195,17 @@ export const Orbit = (props, context) => {
))}
</Section>
<BasicSection
title="Ghosts"
source={ghosts}
searchText={searchText}
/>
<Section title={`Ghosts - (${ghosts.length})`}>
{ghosts
.filter(searchFor(searchText))
.sort(compareNumberedText)
.map(thing => (
<OrbitedButton
key={thing.name}
color="grey"
thing={thing} />
))}
</Section>
<BasicSection
title="Dead"
+114 -128
View File
@@ -3,37 +3,21 @@
* @copyright 2020 WarlockD (https://github.com/warlockd)
* @author Original WarlockD (https://github.com/warlockd)
* @author Changes stylemistake
* @author Changes ThePotato97
* @license MIT
*/
import { classes } from 'common/react';
import { vecScale, vecSubtract } from 'common/vector';
import DOMPurify from 'dompurify';
import { Fragment } from 'inferno';
import { Component } from 'inferno';
import marked from 'marked';
import { useBackend } from '../backend';
import { Box, Flex, Tabs, TextArea } from '../components';
import { Window } from '../layouts';
import { clamp } from 'common/math';
import { sanitizeText } from '../sanitize';
const MAX_PAPER_LENGTH = 5000; // Question, should we send this with ui_data?
const sanatize_text = value => {
// This is VERY important to think first if you NEED
// the tag you put in here. We are pushing all this
// though dangerouslySetInnerHTML and even though
// the default DOMPurify kills javascript, it dosn't
// kill href links or such
return DOMPurify.sanitize(value, {
FORBID_ATTR: ['class', 'style'],
ALLOWED_TAGS: [
'br', 'code', 'li', 'p', 'pre',
'span', 'table', 'td', 'tr',
'th', 'ul', 'ol', 'menu', 'font', 'b',
'center', 'table', 'tr', 'th',
],
});
};
// Hacky, yes, works?...yes
const textWidth = (text, font, fontsize) => {
// default font height is 12 in tgui
@@ -59,11 +43,11 @@ const createIDHeader = index => {
return "paperfield_" + index;
};
// To make a field you do a [_______] or however long the field is
// we will then output a TEXT input for it that hopefuly covers
// we will then output a TEXT input for it that hopefully covers
// the exact amount of spaces
const field_regex = /\[(_+)\]/g;
const field_tag_regex = /\[<input\s+(.*?)id="(?<id>paperfield_\d+)"(.*?)\/>\]/gm;
const sign_regex = /%s(?:ign)?(?=\\s|$)/igm;
const field_tag_regex = /\[<input\s+(?!disabled)(.*?)\s+id="(?<id>paperfield_\d+)"(.*?)\/>\]/gm;
const sign_regex = /%s(?:ign)?(?=\\s|$)?/igm;
const createInputField = (length, width, font,
fontsize, color, id) => {
@@ -156,13 +140,11 @@ const checkAllFields = (txt, font, color, user_name, bold=false) => {
if (dom_text.length === 0) {
continue;
}
const sanitized_text = DOMPurify.sanitize(dom.value.trim(), {
ALLOWED_TAGS: [],
});
const sanitized_text = sanitizeText(dom.value.trim(), []);
if (sanitized_text.length === 0) {
continue;
}
// this is easyer than doing a bunch of text manipulations
// this is easier than doing a bunch of text manipulations
const target = dom.cloneNode(true);
// in case they sign in a field
if (sanitized_text.match(sign_regex)) {
@@ -206,27 +188,25 @@ const Stamp = (props, context) => {
const {
image,
opacity,
...rest
} = props;
const matrix_trasform = 'rotate(' + image.rotate
+ 'deg) translate(' + image.x + 'px,' + image.y + 'px)';
const stamp_trasform = {
'transform': matrix_trasform,
'-ms-transform': matrix_trasform,
'-webkit-transform': matrix_trasform,
const stamp_transform = {
'left': image.x + 'px',
'top': image.y + 'px',
'transform': 'rotate(' + image.rotate + 'deg)',
'opacity': opacity || 1.0,
'position': 'absolute',
};
return (
<div
id="stamp"
className={classes([
'paper121x54',
'Paper__Stamp',
image.sprite,
])}
style={stamp_trasform} />
style={stamp_transform} />
);
};
const setInputReadonly = (text, readonly) => {
return readonly
? text.replace(/<input\s[^d]/g, '<input disabled ')
@@ -237,12 +217,12 @@ const setInputReadonly = (text, readonly) => {
// want to control updates
const PaperSheetView = (props, context) => {
const {
value,
stamps,
value = "",
stamps = [],
backgroundColor,
readOnly,
} = props;
const stamp_list = stamps || [];
const stamp_list = stamps;
const text_html = {
__html: '<span class="paper-text">'
+ setInputReadonly(value, readOnly)
@@ -255,6 +235,8 @@ const PaperSheetView = (props, context) => {
width="100%"
height="100%" >
<Box
className="Paper__Page"
color="black"
fillPositionedParent
width="100%"
height="100%"
@@ -277,70 +259,76 @@ class PaperSheetStamper extends Component {
y: 0,
rotate: 0,
};
this.style = null;
this.handleMouseMove = e => {
const pos = this.findStampPosition(e);
if (!pos) { return; }
// center offset of stamp & rotate
pauseEvent(e);
this.setState({ x: pos[0], y: pos[1], rotate: pos[2] });
};
this.handleMouseClick = e => {
if (e.pageY <= 30) { return; }
const { act, data } = useBackend(this.context);
const stamp_obj = {
x: this.state.x, y: this.state.y, r: this.state.rotate,
stamp_class: this.props.stamp_class,
stamp_icon_state: data.stamp_icon_state,
};
act("stamp", stamp_obj);
};
}
findStampPosition(e) {
const position = {
x: event.pageX,
y: event.pageY,
};
const offset = {
left: e.target.offsetLeft,
top: e.target.offsetTop,
};
let reference = e.target.offsetParent;
while (reference) {
offset.left += reference.offsetLeft;
offset.top += reference.offsetTop;
reference = reference.offsetParent;
let rotating;
const windowRef = document.querySelector('.Layout__content');
if (e.shiftKey) {
rotating = true;
}
const pos = [
position.x - offset.left,
position.y - offset.top,
];
const centerOffset = vecScale([121, 51], 0.5);
const center = vecSubtract(pos, centerOffset);
return center;
if (document.getElementById("stamp"))
{
const stamp = document.getElementById("stamp");
const stampHeight = stamp.clientHeight;
const stampWidth = stamp.clientWidth;
const currentHeight = rotating ? this.state.y : e.pageY
- windowRef.scrollTop - stampHeight;
const currentWidth = rotating ? this.state.x : e.pageX - (stampWidth / 2);
const widthMin = 0;
const heightMin = 0;
const widthMax = (windowRef.clientWidth) - (
stampWidth);
const heightMax = (windowRef.clientHeight - windowRef.scrollTop) - (
stampHeight);
const radians = Math.atan2(
e.pageX - currentWidth,
e.pageY - currentHeight
);
const rotate = rotating ? (radians * (180 / Math.PI) * -1)
: this.state.rotate;
const pos = [
clamp(currentWidth, widthMin, widthMax),
clamp(currentHeight, heightMin, heightMax),
rotate,
];
return pos;
}
}
componentDidMount() {
document.onwheel = this.handleWheel.bind(this);
document.addEventListener("mousemove", this.handleMouseMove);
document.addEventListener("click", this.handleMouseClick);
}
handleMouseMove(e) {
const pos = this.findStampPosition(e);
// center offset of stamp
pauseEvent(e);
this.setState({ x: pos[0], y: pos[1] });
}
handleMouseClick(e) {
const pos = this.findStampPosition(e);
const { act, data } = useBackend(this.context);
const stamp_obj = {
x: pos[0], y: pos[1], r: this.state.rotate,
stamp_class: this.props.stamp_class,
stamp_icon_state: data.stamp_icon_state,
};
act("stamp", stamp_obj);
this.setState({ x: pos[0], y: pos[1] });
}
handleWheel(e) {
const rotate_amount = e.deltaY > 0 ? 15 : -15;
if (e.deltaY < 0 && this.state.rotate === 0) {
this.setState({ rotate: (360+rotate_amount) });
} else if (e.deltaY > 0 && this.state.rotate === 360) {
this.setState({ rotate: rotate_amount });
} else {
const rotate = { rotate: rotate_amount + this.state.rotate };
this.setState(() => rotate);
}
pauseEvent(e);
componentWillUnmount() {
document.removeEventListener("mousemove", this.handleMouseMove);
document.removeEventListener("click", this.handleMouseClick);
}
render() {
@@ -348,7 +336,6 @@ class PaperSheetStamper extends Component {
value,
stamp_class,
stamps,
...rest
} = this.props;
const stamp_list = stamps || [];
const current_pos = {
@@ -358,24 +345,22 @@ class PaperSheetStamper extends Component {
rotate: this.state.rotate,
};
return (
<Box
onClick={this.handleMouseClick.bind(this)}
onMouseMove={this.handleMouseMove.bind(this)}
onwheel={this.handleWheel.bind(this)} {...rest}>
<Fragment>
<PaperSheetView
readOnly
value={value}
stamps={stamp_list} />
<Stamp
active_stamp
opacity={0.5} image={current_pos} />
</Box>
</Fragment>
);
}
}
// ugh. So have to turn this into a full
// component too if I want to keep updates
// low and keep the wierd flashing down
// low and keep the weird flashing down
class PaperSheetEdit extends Component {
constructor(props, context) {
super(props, context);
@@ -401,29 +386,29 @@ class PaperSheetEdit extends Component {
} = data;
const out = { text: text };
// check if we are adding to paper, if not
// we still have to check if somone entered something
// we still have to check if someone entered something
// into the fields
value = value.trim();
if (value.length > 0) {
// First lets make sure it ends in a new line
value += value[value.length] === "\n" ? " \n" : "\n \n";
// Second, we sanatize the text of html
const sanatized_text = sanatize_text(value);
const signed_text = signDocument(sanatized_text, pen_color, edit_usr);
// Second, we sanitize the text of html
const sanitized_text = sanitizeText(value);
const signed_text = signDocument(sanitized_text, pen_color, edit_usr);
// Third we replace the [__] with fields as markedjs fucks them up
const fielded_text = createFields(
signed_text, pen_font, 12, pen_color, field_counter);
// Fourth, parse the text using markup
const formated_text = run_marked_default(fielded_text.text);
const formatted_text = run_marked_default(fielded_text.text);
// Fifth, we wrap the created text in the pin color, and font.
// crayon is bold (<b> tags), mabye make fountain pin italic?
// crayon is bold (<b> tags), maybe make fountain pin italic?
const fonted_text = setFontinText(
formated_text, pen_font, pen_color, is_crayon);
formatted_text, pen_font, pen_color, is_crayon);
out.text += fonted_text;
out.field_counter = fielded_text.counter;
}
if (do_fields) {
// finaly we check all the form fields to see
// finally we check all the form fields to see
// if any data was entered by the user and
// if it was return the data and modify the text
const final_processing = checkAllFields(
@@ -440,7 +425,7 @@ class PaperSheetEdit extends Component {
+ this.state.textarea_text.length;
if (combined_length > MAX_PAPER_LENGTH) {
if ((combined_length - MAX_PAPER_LENGTH) >= value.length) {
// Basicly we cannot add any more text to the paper
// Basically we cannot add any more text to the paper
value = '';
} else {
value = value.substr(0, value.length
@@ -473,15 +458,15 @@ class PaperSheetEdit extends Component {
render() {
const {
value="",
textColor,
fontFamily,
stamps,
backgroundColor,
...rest
} = this.props;
return (
<Flex direction="column" fillPositionedParent>
<Flex
direction="column"
fillPositionedParent>
<Flex.Item>
<Tabs>
<Tabs.Tab
@@ -541,7 +526,7 @@ class PaperSheetEdit extends Component {
this.setState({ previewSelected: "confirm" });
}
}}>
{this.state.previewSelected === "confirm" ? "confirm" : "save"}
{this.state.previewSelected === "confirm" ? "Confirm" : "Save"}
</Tabs.Tab>
</Tabs>
</Flex.Item>
@@ -574,19 +559,16 @@ export const PaperSheet = (props, context) => {
const {
edit_mode,
text,
paper_color,
paper_color = "white",
pen_color = "black",
pen_font = "Verdana",
stamps,
stamp_class,
stamped,
sizeX,
sizeY,
name,
} = data;
// You might ask why? Because Window/window content do wierd
// css stuff with white for some reason
const backgroundColor = paper_color && paper_color !== "white"
? paper_color
: "#FFFFFF";
const stamp_list = !stamps || stamps === null
const stamp_list = !stamps
? []
: stamps;
const decide_mode = mode => {
@@ -605,7 +587,7 @@ export const PaperSheet = (props, context) => {
textColor={pen_color}
fontFamily={pen_font}
stamps={stamp_list}
backgroundColor={backgroundColor} />
backgroundColor={paper_color} />
);
case 2:
return (
@@ -620,14 +602,18 @@ export const PaperSheet = (props, context) => {
};
return (
<Window
title={name}
theme="paper"
width={400}
height={500}
width={sizeX || 400}
height={sizeY || 500}
resizable>
<Window.Content>
<Window.Content
backgroundColor={paper_color}
scrollable>
<Box
fillPositionedParent
backgroundColor={backgroundColor}>
id="page"
fitted
fillPositionedParent>
{decide_mode(edit_mode)}
</Box>
</Window.Content>
@@ -45,6 +45,7 @@ export const Photocopier = (props, context) => {
const Toner = (props, context) => {
const { act, data } = useBackend(context);
const {
has_toner,
max_toner,
current_toner,
} = data;
@@ -57,6 +58,7 @@ const Toner = (props, context) => {
title="Toner"
buttons={
<Button
disabled={!has_toner}
onClick={() => act('remove_toner')}
icon="eject">
Eject
@@ -10,7 +10,7 @@ export const PortableScrubber = (props, context) => {
return (
<Window
width={320}
height={350}>
height={376}>
<Window.Content>
<PortableBasicInfo />
<Section title="Filters">
@@ -0,0 +1,147 @@
import { resolveAsset } from '../assets';
import { useBackend, useLocalState } from '../backend';
import { Button, Flex, NoticeBox, Section, Tabs } from '../components';
import { Window } from '../layouts';
export const PortraitPicker = (props, context) => {
const { act, data } = useBackend(context);
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [listIndex, setListIndex] = useLocalState(context, 'listIndex', 0);
const {
library,
library_secure,
library_private,
} = data;
const TABS = [
{
name: 'Common Portraits',
list: library,
},
{
name: 'Secure Portraits',
list: library_secure,
},
{
name: 'Private Portraits',
list: library_private,
},
];
const tab2list = TABS[tabIndex].list;
const current_portrait = tab2list[listIndex]["title"];
return (
<Window
theme="ntos"
title="Portrait Picker"
width={400}
height={406}>
<Window.Content>
<Flex height="100%" direction="column">
<Flex.Item mb={1}>
<Section fitted>
<Tabs fluid textAlign="center">
{TABS.map((tabObj, i) => (
<Tabs.Tab
key={i}
selected={i === tabIndex}
onClick={() => {
setListIndex(0);
setTabIndex(i);
}}>
{tabObj.name}
</Tabs.Tab>
))}
</Tabs>
</Section>
</Flex.Item>
<Flex.Item mb={1} grow={2}>
<Section fill>
<Flex
height="100%"
align="center"
justify="center"
direction="column">
<Flex.Item>
<img
src={resolveAsset(current_portrait)}
height="96px"
width="96px"
style={{
'vertical-align': 'middle',
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
</Flex.Item>
<Flex.Item className="Section__titleText">
{current_portrait}
</Flex.Item>
</Flex>
</Section>
</Flex.Item>
<Flex.Item>
<Flex>
<Flex.Item grow={3}>
<Section height="100%">
<Flex justify="space-between">
<Flex.Item grow={1}>
<Button
icon="angle-double-left"
disabled={listIndex === 0}
onClick={() => setListIndex(0)}
/>
</Flex.Item>
<Flex.Item grow={3}>
<Button
disabled={listIndex === 0}
icon="chevron-left"
onClick={() => setListIndex(listIndex-1)}
/>
</Flex.Item>
<Flex.Item grow={3}>
<Button
icon="check"
content="Select Portrait"
onClick={() => act("select", {
tab: tabIndex+1,
selected: listIndex+1,
})}
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="chevron-right"
disabled={listIndex === tab2list.length-1}
onClick={() => setListIndex(listIndex+1)}
/>
</Flex.Item>
<Flex.Item>
<Button
icon="angle-double-right"
disabled={listIndex === tab2list.length-1}
onClick={() => setListIndex(tab2list.length-1)}
/>
</Flex.Item>
</Flex>
</Section>
</Flex.Item>
</Flex>
<Flex.Item mt={1}>
<NoticeBox info>
Only the 23x23 or 24x24 canvas size art can be
displayed. Make sure you read the warning below
before embracing the wide wonderful world of
artistic expression!
</NoticeBox>
</Flex.Item>
<Flex.Item>
<NoticeBox danger>
WARNING: While Central Command loves art as much as you do,
choosing erotic art will lead to severe consequences.
Additionally, Central Command reserves the right to request
you change your display portrait, for any reason.
</NoticeBox>
</Flex.Item>
</Flex.Item>
</Flex>
</Window.Content>
</Window>
);
};
+2 -2
View File
@@ -42,7 +42,7 @@ export const RouletteNumberButton = (props, context) => {
textAlign="center"
mb={0}
className="Roulette__board-extrabutton"
onClick={() => act('ChangeBetType', { type: number })}
onClick={() => act('ChangeBetType', { type: number.toString() })}
/>
);
};
@@ -70,7 +70,7 @@ export const RouletteBoard = (props, context) => {
color="transparent"
height="88px"
className="Roulette__board-extrabutton"
onClick={() => act('ChangeBetType', { type: 0 })}
onClick={() => act('ChangeBetType', { type: "0" })}
/>
</td>
{firstRow.map(number => (
+149
View File
@@ -0,0 +1,149 @@
import { Fragment } from 'inferno';
import { resolveAsset } from '../assets';
import { useBackend } from '../backend';
import { Box, Button, Icon, Section } from '../components';
import { Window } from "../layouts";
export const Safe = (properties, context) => {
const { act, data } = useBackend(context);
const {
dial,
open,
} = data;
return (
<Window
width={625}
height={800}
theme="ntos">
<Window.Content>
<Box className="Safe__engraving">
<Dialer />
<Box>
<Box
className="Safe__engraving-hinge"
top="25%" />
<Box
className="Safe__engraving-hinge"
top="75%" />
</Box>
<Icon
className="Safe__engraving-arrow"
name="long-arrow-alt-down"
size="5"
/><br />
{open ? (
<Contents />
) : (
<Box
as="img"
className="Safe__dial"
src={resolveAsset('safe_dial.png')}
style={{
"transform": "rotate(-" + (3.6 * dial) + "deg)",
}}
/>
)}
</Box>
{!open && (
<Help />
)}
</Window.Content>
</Window>
);
};
const Dialer = (properties, context) => {
const { act, data } = useBackend(context);
const {
dial,
open,
locked,
broken,
} = data;
const dialButton = (amount, right) => {
return (
<Button
disabled={open || (right && !locked) || broken}
icon={"arrow-" + (right ? "right" : "left")}
content={(right ? "Right" : "Left") + " " + amount}
iconPosition={right ? "right" : "left"}
onClick={() => act(!right ? "turnright" : "turnleft", {
num: amount,
})}
/>
);
};
return (
<Box className="Safe__dialer">
<Button
disabled={locked && !broken}
icon={open ? "lock" : "lock-open"}
content={open ? "Close" : "Open"}
mb="0.5rem"
onClick={() => act('open')}
/><br />
<Box position="absolute">
{[dialButton(50), dialButton(10), dialButton(1)]}
</Box>
<Box
className="Safe__dialer-right"
position="absolute" right="5px">
{[dialButton(1, true), dialButton(10, true), dialButton(50, true)]}
</Box>
<Box className="Safe__dialer-number">
{dial}
</Box>
</Box>
);
};
const Contents = (properties, context) => {
const { act, data } = useBackend(context);
const {
contents,
} = data;
return (
<Box
className="Safe__contents"
overflow="auto">
{contents.map((item, index) => (
<Fragment key={item}>
<Button
mb="0.5rem"
onClick={() => act("retrieve", {
index: index + 1,
})}>
<Box
as="img"
src={item.sprite + ".png"}
verticalAlign="middle"
ml="-6px"
mr="0.5rem"
/>
{item.name}
</Button>
<br />
</Fragment>
))}
</Box>
);
};
const Help = (properties, context) => {
return (
<Section
className="Safe__help"
title="Safe opening instructions (because you all keep forgetting)">
<Box>
1. Turn the dial left to the first number.<br />
2. Turn the dial right to the second number.<br />
3. Continue repeating this process for each number,
switching between left and right each time.<br />
4. Open the safe.
</Box>
<Box bold>
To lock fully, turn the dial to the left after closing the safe.
</Box>
</Section>
);
};
+678
View File
@@ -0,0 +1,678 @@
import { toFixed } from 'common/math';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Button, Flex, LabeledControls, NoticeBox, RoundGauge, Section } from '../components';
import { FlexItem } from '../components/Flex';
import { Window } from '../layouts';
const TAB2NAME = [
{
title: 'Debugging',
blurb: 'Where useless shit goes to die',
gauge: 5,
component: () => DebuggingTab,
},
{
title: 'Helpful',
blurb: 'Where fuckwits put logging',
gauge: 25,
component: () => HelpfulTab,
},
{
title: 'Fun',
blurb: 'How I ran an """event"""',
gauge: 75,
component: () => FunTab,
},
{
title: 'Only Fun For You',
blurb: 'How I spent my last day adminning',
gauge: 95,
component: () => FunForYouTab,
},
];
const lineHeightNormal = 2.79;
const lineHeightDebug = 6;
const DebuggingTab = (props, context) => {
const { act } = useBackend(context);
return (
<Flex
grow={1}
mb={-0.25}
mx={-0.5}
direction="column"
height="100%"
textAlign="center"
align="stretch"
justify="center">
<Flex.Item my={0.5}>
<Button
lineHeight={lineHeightDebug}
icon="question"
fluid
content="Change all maintenance doors to engie/brig access only"
onClick={() => act("maint_access_engiebrig")} />
</Flex.Item>
<Flex.Item my={0.5}>
<Button
lineHeight={lineHeightDebug}
icon="question"
fluid
content="Change all maintenance doors to brig access only"
onClick={() => act("maint_access_brig")} />
</Flex.Item>
<Flex.Item mt={0.5} mb={-0.5}>
<Button
lineHeight={lineHeightDebug}
icon="question"
fluid
content="Remove cap on security officers"
onClick={() => act("infinite_sec")} />
</Flex.Item>
</Flex>
);
};
const HelpfulTab = (props, context) => {
const { act } = useBackend(context);
return (
<Flex direction="column" mb={-0.75} mx={-0.5}>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="plus"
lineHeight={lineHeightNormal}
fluid
content="Cure all diseases currently in existence"
onClick={() => act("clear_virus")} />
</Flex.Item>
<Flex.Item grow={1} ml={0.5}>
<Button
icon="eye"
lineHeight={lineHeightNormal}
fluid
content="Show Gamemode"
onClick={() => act("showgm")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="bomb"
lineHeight={lineHeightNormal}
fluid
content="List Bombers"
onClick={() => act("list_bombers")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="signal"
lineHeight={lineHeightNormal}
fluid
content="List Signalers"
onClick={() => act("list_signalers")} />
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="robot"
lineHeight={lineHeightNormal}
fluid
content="List laws"
onClick={() => act("list_lawchanges")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="address-book"
lineHeight={lineHeightNormal}
fluid
content="Show Manifest"
onClick={() => act("manifest")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="dna"
lineHeight={lineHeightNormal}
fluid
content="Show DNA"
onClick={() => act("dna")} />
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="fingerprint"
lineHeight={lineHeightNormal}
fluid
content="Show Fingerprints"
onClick={() => act("fingerprints")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="flag"
lineHeight={lineHeightNormal}
fluid
content="Toggle CTF"
onClick={() => act("ctfbutton")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="sync-alt"
lineHeight={lineHeightNormal}
fluid
content="Reset Thunderdome"
onClick={() => act("tdomereset")} />
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="moon"
lineHeight={lineHeightNormal}
fluid
content="Set Nightshift"
onClick={() => act("night_shift_set")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="pencil-alt"
lineHeight={lineHeightNormal}
fluid
content="Rename Station"
onClick={() => act("set_name")} />
</Flex.Item>
<Flex.Item grow={1} ml={0.5}>
<Button
icon="eraser"
lineHeight={lineHeightNormal}
fluid
content="Reset Station"
onClick={() => act("reset_name")} />
</Flex.Item>
</Flex>
<Flex
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="plane-departure"
lineHeight={lineHeightNormal}
fluid
content="Move Ferry"
onClick={() => act("moveferry")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="plane"
lineHeight={lineHeightNormal}
fluid
content="Toggle Arrivals"
onClick={() => act("togglearrivals")} />
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="plane-arrival"
lineHeight={lineHeightNormal}
fluid
content="Move Labor"
onClick={() => act("movelaborshuttle")} />
</Flex.Item>
</Flex>
</Flex>
);
};
const FunTab = (props, context) => {
const { act } = useBackend(context);
return (
<Flex direction="column" mb={-0.75} mx={-0.5} textAlign="center">
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="grin-beam-sweat"
lineHeight={lineHeightNormal}
fluid
content="Break all lights"
onClick={() => act("blackout")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="magic"
lineHeight={lineHeightNormal}
fluid
content="Fix all lights"
onClick={() => act("whiteout")} />
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="biohazard"
lineHeight={lineHeightNormal}
fluid
content="Trigger Outbreak"
onClick={() => act("virus")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item>
<Button
icon="bolt"
lineHeight={lineHeightNormal}
fluid
content="All areas powered"
onClick={() => act("power")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="moon"
lineHeight={lineHeightNormal}
fluid
content="All areas unpowered"
onClick={() => act("unpower")} />
</Flex.Item>
<Flex.Item>
<Button
icon="plug"
lineHeight={lineHeightNormal}
fluid
content="IC power (SMES charged)"
onClick={() => act("quickpower")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="user-ninja"
lineHeight={lineHeightNormal}
fluid
content="Anonymous Names"
onClick={() => act("anon_name")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="users"
lineHeight={lineHeightNormal}
fluid
content="Triple AI mode"
onClick={() => act("tripleAI")} />
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="bullhorn"
lineHeight={lineHeightNormal}
fluid
content="THERE CAN ONLY BE ONE!"
onClick={() => act("onlyone")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="grin-beam-sweat"
lineHeight={lineHeightNormal}
fluid
content="Summon Guns"
onClick={() => act("guns")} />
</Flex.Item>
<Flex.Item grow={1} mx={0.5}>
<Button
icon="magic"
lineHeight={lineHeightNormal}
fluid
content="Summon Magic"
onClick={() => act("magic")} />
</Flex.Item>
<Flex.Item grow={1}>
<Button
icon="meteor"
lineHeight={lineHeightNormal}
fluid
content="Summon Events"
onClick={() => act("events")} />
</Flex.Item>
</Flex>
<Flex
mb={1}
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="hammer"
lineHeight={lineHeightNormal}
fluid
content="Egalitarian Station"
onClick={() => act("eagles")} />
</Flex.Item>
<Flex.Item grow={1} ml={0.5}>
<Button
icon="dollar-sign"
lineHeight={lineHeightNormal}
fluid
content="Anarcho-Capitalist Station"
onClick={() => act("ancap")} />
</Flex.Item>
</Flex>
<Flex
grow={1}
direction="row"
height="100%"
align="stretch"
justify="space-between">
<Flex.Item grow={1}>
<Button
icon="bullseye"
lineHeight={lineHeightNormal}
fluid
content="Custom Portal Storm"
onClick={() => act("customportal")} />
</Flex.Item>
<Flex.Item grow={1} ml={0.5}>
<Button
icon="bomb"
lineHeight={lineHeightNormal}
fluid
content="Change Bomb Cap"
onClick={() => act("changebombcap")} />
</Flex.Item>
</Flex>
</Flex>
);
};
const FunForYouTab = (props, context) => {
const { act } = useBackend(context);
return (
<Flex
grow={1}
mx={-0.5}
mb={-1.75}
direction="column"
height="100%"
align="stretch"
justify="center">
<Flex.Item>
<NoticeBox danger>
<Button
color="red"
icon="paw"
fluid
content="Turn all humans into monkeys"
onClick={() => act("monkey")} />
</NoticeBox>
</Flex.Item>
<Flex.Item>
<NoticeBox danger>
<Button
color="red"
icon="user-secret"
fluid
content="Everyone is the traitor"
onClick={() => act("traitor_all")} />
</NoticeBox>
</Flex.Item>
<Flex.Item>
<NoticeBox danger>
<Button
color="red"
icon="brain"
fluid
content="Make all players brain damaged"
onClick={() => act("massbraindamage")} />
</NoticeBox>
</Flex.Item>
<Flex.Item>
<NoticeBox danger>
<Button
color="black"
icon="fire"
fluid
content="The floor is lava! (DANGEROUS: extremely lame)"
onClick={() => act("floorlava")} />
</NoticeBox>
</Flex.Item>
<Flex.Item>
<NoticeBox danger>
<Button
color="black"
icon="tired"
fluid
content="Chinese Cartoons! (DANGEROUS: no going back, also fuck you)"
onClick={() => act("anime")} />
</NoticeBox>
</Flex.Item>
<Flex.Item>
<Flex>
<Flex.Item width="240px" mr={0.25}>
<NoticeBox danger>
<Button
color="red"
icon="cat"
fluid
content="Mass Purrbation"
onClick={() => act("masspurrbation")} />
</NoticeBox>
</Flex.Item>
<Flex.Item grow={1} ml={0.25}>
<NoticeBox info>
<Button
color="blue"
icon="user"
fluid
content="Mass Remove Purrbation"
onClick={() => act("massremovepurrbation")} />
</NoticeBox>
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item>
<Flex justify="space-between">
<Flex.Item width="240px" mr={0.25}>
<NoticeBox danger>
<Button
color="red"
icon="flushed"
fluid
content="Fully Immerse Everyone"
onClick={() => act("massimmerse")} />
</NoticeBox>
</Flex.Item>
<Flex.Item grow={1} ml={0.25}>
<NoticeBox info>
<Button
color="blue"
icon="sync-alt"
fluid
content="Shatter the Immersion"
onClick={() => act("unmassimmerse")} />
</NoticeBox>
</Flex.Item>
</Flex>
</Flex.Item>
</Flex>
);
};
export const Secrets = (props, context) => {
const { act, data } = useBackend(context);
const {
is_debugger,
is_funmin,
} = data;
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'tab-index', 2);
const TabComponent = TAB2NAME[tabIndex-1].component();
return (
<Window
title="Secrets Panel"
width={500}
height={485}>
<Window.Content>
<Flex direction="column" height="100%">
<Flex.Item mb={1}>
<Section
title="Secrets"
buttons={(
<Fragment>
<Button
color="blue"
icon="address-card"
content="Admin Log"
onClick={() => act("admin_log")} />
<Button
color="blue"
icon="eye"
content="Show Admins"
onClick={() => act("show_admins")} />
</Fragment>)}>
<Flex
mx={-0.5}
align="stretch"
justify="center">
<Flex.Item bold>
<NoticeBox color="black">
&quot;The first rule of adminbuse is:
you don&apos;t talk about the adminbuse.&quot;
</NoticeBox>
</Flex.Item>
</Flex>
<Flex
textAlign="center"
mx={-0.5}
align="stretch"
justify="center">
<Flex.Item ml={-10} mr={1}>
<Button
selected={tabIndex === 2}
icon="check-circle"
content="Helpful"
onClick={() => setTabIndex(2)} />
</Flex.Item>
<Flex.Item ml={1}>
<Button
disabled={is_funmin === 0}
selected={tabIndex === 3}
icon="smile"
content="Fun"
onClick={() => setTabIndex(3)} />
</Flex.Item>
</Flex>
<Flex
mx={-0.5}
align="stretch"
justify="center">
<Flex.Item mt={1}>
<Button
disabled={is_debugger === 0}
selected={tabIndex === 1}
icon="glasses"
content="Debugging"
onClick={() => setTabIndex(1)} />
</Flex.Item>
<Flex.Item>
<LabeledControls>
<LabeledControls.Item
minWidth="66px"
label="Chances of admin complaint">
<RoundGauge
size={2}
value={TAB2NAME[tabIndex-1].gauge}
minValue={0}
maxValue={100}
alertAfter={100 * 0.70}
ranges={{
"good": [-2, 100 * 0.25],
"average": [100 * 0.25, 100 * 0.75],
"bad": [100 * 0.75, 100],
}}
format={value => toFixed(value) + '%'} />
</LabeledControls.Item>
</LabeledControls>
</Flex.Item>
<Flex.Item mt={1}>
<Button
disabled={is_funmin === 0}
selected={tabIndex === 4}
icon="smile-wink"
content="Only Fun For You"
onClick={() => setTabIndex(4)} />
</Flex.Item>
</Flex>
</Section>
</Flex.Item>
<Flex.Item grow={1}>
<Section
fill={false}
title={TAB2NAME[tabIndex-1].title
+" Or: "+TAB2NAME[tabIndex-1].blurb}>
<TabComponent />
</Section>
</Flex.Item>
</Flex>
</Window.Content>
</Window>
);
};
+18 -10
View File
@@ -59,6 +59,16 @@ const getLocationIdByName = (locations, name) => {
return locations?.find(location => location.name === name)?.id;
};
const STATUS_COLOR_KEYS = {
"In Transit": "good",
"Idle": "average",
"Igniting": "average",
"Recharging": "average",
"Missing": "bad",
"Unauthorized Access": "bad",
"Locked": "bad",
};
const ShuttleConsoleContent = (props, context) => {
const { act, data } = useBackend(context);
const {
@@ -90,13 +100,7 @@ const ShuttleConsoleContent = (props, context) => {
</Box>
<Box
inline
color={status==="In Transit"
? 'good'
: status==="Idle"
? 'average'
: status==="Igniting"
? 'average'
: 'bad'}
color={STATUS_COLOR_KEYS[status] || "bad"}
ml={1}>
{status || "Not Available"}
</Box>
@@ -110,15 +114,20 @@ const ShuttleConsoleContent = (props, context) => {
</LabeledList.Item>
<LabeledList.Item label="Destination">
{locations.length===0 && (
<Box color="bad">
<Box
mb={1.7}
color="bad">
Not Available
</Box>
) || locations.length===1 &&(
<Box color="average">
<Box
mb={1.7}
color="average">
{getLocationNameById(locations, destination)}
</Box>
) || (
<Dropdown
mb={1.7}
over
width="240px"
options={locations.map(location => location.name)}
@@ -134,7 +143,6 @@ const ShuttleConsoleContent = (props, context) => {
content="Depart"
disabled={!getLocationNameById(locations, destination)
|| locked || authorization_required}
mt={1.5}
icon="arrow-up"
textAlign="center"
onClick={() => act('move', {
@@ -109,7 +109,7 @@ export const ShuttleManipulatorTemplates = (props, context) => {
selectedTemplateId,
setSelectedTemplateId,
] = useLocalState(context, 'templateId', Object.keys(templateObject)[0]);
const actualTemplates = templateObject[selectedTemplateId]?.templates;
const actualTemplates = templateObject[selectedTemplateId]?.templates || [];
return (
<Section>
<Flex>
@@ -227,15 +227,21 @@ export const ShuttleManipulatorModification = (props, context) => {
<Section
level={2}
title="Status">
<Button
content="Load"
color="good"
onClick={() => act('load', {
shuttle_id: selected.shuttle_id,
})} />
<Button
content="Preview"
onClick={() => act('preview', {
shuttle_id: selected.shuttle_id,
})} />
<Button
content="Load"
content="Replace"
color="bad"
onClick={() => act('load', {
onClick={() => act('replace', {
shuttle_id: selected.shuttle_id,
})} />
</Section>
@@ -1,7 +1,8 @@
import { useBackend } from '../backend';
import { Button, Box, Section, NoticeBox, TimeDisplay, Flex, Icon, Table, Fragment, LabeledList } from '../components';
import { Window } from '../layouts';
import { toFixed } from 'common/math';
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, Flex, Icon, LabeledList, NoticeBox, Section, Table } from '../components';
import { Window } from '../layouts';
export const InsertedSkillchip = (props, context) => {
const { act, data } = useBackend(context);
+206
View File
@@ -0,0 +1,206 @@
import { createSearch } from 'common/string';
import { Fragment } from 'inferno';
import { sortBy } from 'common/collections';
import { useBackend, useLocalState } from "../backend";
import { Box, Button, Input, NoticeBox, Section, Collapsible, Table } from "../components";
import { Window } from "../layouts";
export const Stack = (props, context) => {
const { act, data } = useBackend(context);
const {
amount,
recipes = [],
} = data;
const [
searchText,
setSearchText,
] = useLocalState(context, 'searchText', '');
const testSearch = createSearch(searchText, item => {
return item;
});
const items = searchText.length > 0
&& Object.keys(recipes)
.filter(testSearch)
.reduce((obj, key) => {
obj[key] = recipes[key];
return obj;
}, {})
|| recipes;
const height = Math.max(94 + Object.keys(recipes).length * 26, 250);
return (
<Window
width={400}
height={Math.min(height, 500)}
resizable>
<Window.Content scrollable>
<Section
title={"Amount: " + amount}
buttons={(
<Fragment>
Search
<Input
autoFocus
value={searchText}
onInput={(e, value) => setSearchText(value)}
mx={1} />
</Fragment>
)}>
{items.length === 0 && (
<NoticeBox>
No recipes found.
</NoticeBox>
) || (
<RecipeList recipes={items} />
)}
</Section>
</Window.Content>
</Window>
);
};
const RecipeList = (props, context) => {
const { act, data } = useBackend(context);
const {
recipes,
} = props;
const sortedKeys = sortBy(key => key.toLowerCase())(Object.keys(recipes));
return sortedKeys.map(title => {
const recipe = recipes[title];
if (recipe.ref === undefined) {
return (
<Collapsible
ml={1}
color="label"
title={title}>
<Box ml={1}>
<RecipeList recipes={recipe} />
</Box>
</Collapsible>
);
} else {
return (
<Recipe
title={title}
recipe={recipe} />
);
}
});
};
const buildMultiplier = (recipe, amount) => {
if (recipe.req_amount > amount) {
return 0;
}
return Math.floor(amount / recipe.req_amount);
};
const Multipliers = (props, context) => {
const { act, data } = useBackend(context);
const {
recipe,
maxMultiplier,
} = props;
const maxM = Math.min(maxMultiplier,
Math.floor(recipe.max_res_amount / recipe.res_amount));
const multipliers = [5, 10, 25];
let finalResult = [];
for (const multiplier of multipliers) {
if (maxM >= multiplier) {
finalResult.push((
<Button
content={multiplier * recipe.res_amount + "x"}
onClick={() => act("make", {
ref: recipe.ref,
multiplier: multiplier,
})} />
));
}
}
if (multipliers.indexOf(maxM) === -1) {
finalResult.push((
<Button
content={maxM * recipe.res_amount + "x"}
onClick={() => act("make", {
ref: recipe.ref,
multiplier: maxM,
})} />
));
}
return finalResult;
};
const Recipe = (props, context) => {
const { act, data } = useBackend(context);
const {
amount,
} = data;
const {
recipe,
title,
} = props;
const {
res_amount,
max_res_amount,
req_amount,
ref,
} = recipe;
let buttonName = title;
buttonName += " (";
buttonName += req_amount + " ";
buttonName += ("sheet" + (req_amount > 1 ? "s" : ""));
buttonName += ")";
if (res_amount > 1) {
buttonName = res_amount + "x " + buttonName;
}
const maxMultiplier = buildMultiplier(recipe, amount);
return (
<Box mb={1}>
<Table>
<Table.Row>
<Table.Cell>
<Button
fluid
disabled={!maxMultiplier}
icon="wrench"
content={buttonName}
onClick={() => act("make", {
ref: recipe.ref,
multiplier: 1,
})} />
</Table.Cell>
{max_res_amount > 1 && maxMultiplier > 1 && (
<Table.Cell collapsing>
<Multipliers
recipe={recipe}
maxMultiplier={maxMultiplier} />
</Table.Cell>
)}
</Table.Row>
</Table>
</Box>
);
};
@@ -0,0 +1,71 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Button, LabeledList, NoticeBox, Section } from '../components';
import { Window } from '../layouts';
export const StackingConsole = (props, context) => {
const { act, data } = useBackend(context);
const {
machine,
} = data;
return (
<Window
width={320}
height={340}
resizable>
<Window.Content scrollable>
{!machine ? (
<NoticeBox>
No connected stacking machine
</NoticeBox>
) : (
<StackingConsoleContent />
)}
</Window.Content>
</Window>
);
};
export const StackingConsoleContent = (props, context) => {
const { act, data } = useBackend(context);
const {
stacking_amount,
contents = [],
} = data;
return (
<Fragment>
<Section>
<LabeledList>
<LabeledList.Item label="Stacking Amount">
{stacking_amount || "Unknown"}
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Stored Materials">
{!contents.length ? (
<NoticeBox>
No stored materials
</NoticeBox>
) : (
<LabeledList>
{contents.map(sheet => (
<LabeledList.Item
key={sheet.type}
label={sheet.name}
buttons={(
<Button
icon="eject"
content="Release"
onClick={() => act('release', {
type: sheet.type,
})} />
)}>
{sheet.amount || "Unknown"}
</LabeledList.Item>
))}
</LabeledList>
)}
</Section>
</Fragment>
);
};
+39 -16
View File
@@ -1,28 +1,51 @@
import { toFixed } from 'common/math';
import { useBackend } from '../backend';
import { Button, LabeledList, NumberInput, ProgressBar, Section } from '../components';
import { Button, LabeledControls, NumberInput, RoundGauge, Section } from '../components';
import { formatSiUnit } from '../format';
import { Window } from '../layouts';
const formatPressure = value => {
if (value < 10000) {
return toFixed(value) + ' kPa';
}
return formatSiUnit(value * 1000, 1, 'Pa');
};
export const Tank = (props, context) => {
const { act, data } = useBackend(context);
const {
defaultReleasePressure,
minReleasePressure,
maxReleasePressure,
leakPressure,
fragmentPressure,
tankPressure,
releasePressure,
connected,
} = data;
return (
<Window
width={400}
resizable
width={275}
height={120}>
<Window.Content>
<Section>
<LabeledList>
<LabeledList.Item label="Pressure">
<ProgressBar
value={data.tankPressure / 1013}
<LabeledControls>
<LabeledControls.Item label="Pressure">
<RoundGauge
value={tankPressure}
minValue={0}
maxValue={fragmentPressure * 1.15}
alertAfter={leakPressure}
ranges={{
good: [0.35, Infinity],
average: [0.15, 0.35],
bad: [-Infinity, 0.15],
}}>
{data.tankPressure + ' kPa'}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Pressure Regulator">
"good": [0, leakPressure],
"average": [leakPressure, fragmentPressure],
"bad": [fragmentPressure, fragmentPressure * 1.15],
}}
format={formatPressure}
size={2} />
</LabeledControls.Item>
<LabeledControls.Item label="Pressure Regulator">
<Button
icon="fast-backward"
disabled={data.ReleasePressure === data.minReleasePressure}
@@ -52,8 +75,8 @@ export const Tank = (props, context) => {
onClick={() => act('pressure', {
pressure: 'reset',
})} />
</LabeledList.Item>
</LabeledList>
</LabeledControls.Item>
</LabeledControls>
</Section>
</Window.Content>
</Window>
+12 -19
View File
@@ -14,8 +14,8 @@ export const Teleporter = (props, context) => {
} = data;
return (
<Window
width={470}
height={140}>
width={360}
height={130}>
<Window.Content>
<Section>
{!power_station && (
@@ -28,29 +28,22 @@ export const Teleporter = (props, context) => {
</Box>
)) || (
<LabeledList>
<LabeledList.Item label="Current Regime"
buttons={(
<Button
icon="tools"
content="Change Regime"
onClick={() => act('regimeset')} />
)}>
{regime_set}
<LabeledList.Item label="Regime">
<Button
content={regime_set}
onClick={() => act('regimeset')} />
</LabeledList.Item>
<LabeledList.Item label="Current Target"
buttons={(
<Button
icon="tools"
content="Set Target"
onClick={() => act('settarget')} />
)}>
{target}
<LabeledList.Item label="Target">
<Button
icon="edit"
content={target}
onClick={() => act('settarget')} />
</LabeledList.Item>
<LabeledList.Item label="Calibration"
buttons={(
<Button
icon="tools"
content="Calibrate Hub"
content="Calibrate"
onClick={() => act('calibrate')} />
)}>
{calibrating && (
@@ -8,7 +8,7 @@ export const ThermoMachine = (props, context) => {
return (
<Window
width={300}
height={230}>
height={250}>
<Window.Content>
<Section title="Status">
<LabeledList>
@@ -36,6 +36,13 @@ export const ThermoMachine = (props, context) => {
onClick={() => act('power')} />
)}>
<LabeledList>
<LabeledList.Item label="Setting">
<Button
icon={data.cooling ? 'cooling' : 'heating'}
content={data.cooling ? 'Cooling' : 'Heating'}
selected={data.cooling}
onClick={() => act('cooling')} />
</LabeledList.Item>
<LabeledList.Item label="Target Temperature">
<NumberInput
animated
@@ -0,0 +1,102 @@
import { sortBy } from "common/collections";
import { useBackend } from "../backend";
import { Box, Flex, ProgressBar, Section, Table } from "../components";
import { Window } from "../layouts";
const JOB_REPORT_MENU_FAIL_REASON_TRACKING_DISABLED = 1;
const JOB_REPORT_MENU_FAIL_REASON_NO_RECORDS = 2;
const sortByPlaytime = sortBy(([_, playtime]) => -playtime);
const PlaytimeSection = props => {
const { playtimes } = props;
const sortedPlaytimes = sortByPlaytime(Object.entries(playtimes));
const mostPlayed = sortedPlaytimes[0][1];
return (
<Table>
{sortedPlaytimes.map(([jobName, playtime]) => {
const ratio = playtime / mostPlayed;
return (
<Table.Row key={jobName}>
<Table.Cell collapsing p={0.5} style={{
"vertical-align": "middle",
}}>
<Box align="right">{jobName}</Box>
</Table.Cell>
<Table.Cell>
<ProgressBar
maxValue={mostPlayed}
value={playtime}>
<Flex>
<Flex.Item width={`${ratio * 100}%`} />
<Flex.Item>
{(playtime / 60).toLocaleString(undefined, {
"minimumFractionDigits": 1,
"maximumFractionDigits": 1,
})}h
</Flex.Item>
</Flex>
</ProgressBar>
</Table.Cell>
</Table.Row>
);
})}
</Table>
);
};
export const TrackedPlaytime = (props, context) => {
const { data } = useBackend(context);
const {
failReason,
jobPlaytimes,
specialPlaytimes,
livingTime,
ghostTime,
} = data;
return (
<Window
title="Tracked Playtime"
width={550}
height={650}
resizable>
<Window.Content scrollable>
{failReason && (
failReason === JOB_REPORT_MENU_FAIL_REASON_TRACKING_DISABLED
&& <Box>This server has disabled tracking.</Box>
|| failReason === JOB_REPORT_MENU_FAIL_REASON_NO_RECORDS
&& <Box>You have no records.</Box>
) || (
<Box>
<Section title="Total">
<PlaytimeSection
playtimes={{
"Ghost": ghostTime,
"Living": livingTime,
}}
/>
</Section>
<Section title="Jobs">
<PlaytimeSection
playtimes={jobPlaytimes}
/>
</Section>
<Section title="Special">
<PlaytimeSection
playtimes={specialPlaytimes}
/>
</Section>
</Box>
)}
</Window.Content>
</Window>
);
};
@@ -14,7 +14,7 @@ export const TurretControl = (props, context) => {
return (
<Window
width={305}
height={172}>
height={data.siliconUser ? 168 : 164}>
<Window.Content>
<InterfaceLockNoticeBox />
<Section>
+6 -3
View File
@@ -14,6 +14,7 @@ const VendingRow = (props, context) => {
onstation,
department,
user,
jobDiscount,
} = data;
const free = (
!onstation
@@ -22,9 +23,10 @@ const VendingRow = (props, context) => {
!product.premium
&& department
&& user
&& department === user.department
)
);
const discount = department === user?.department;
const redPrice = Math.round(product.price * jobDiscount);
return (
<Table.Row>
<Table.Cell collapsing>
@@ -79,7 +81,8 @@ const VendingRow = (props, context) => {
|| product.price > data.user.cash
)
)}
content={free ? 'FREE' : product.price + ' cr'}
content={(free && discount)
? `${redPrice} cr` : `${product.price} cr`}
onClick={() => act('vend', {
'ref': product.ref,
})} />
@@ -102,7 +105,7 @@ export const Vending = (props, context) => {
let inventory;
let custom = false;
if (data.vending_machine_input) {
inventory = data.vending_machine_input || [];
inventory = data.vending_machine_input;
custom = true;
}
else {
@@ -1,9 +1,8 @@
import { decodeHtmlEntities } from 'common/string';
import { useBackend, useLocalState } from '../../backend';
import { Box, Button, LabeledList, NumberInput, Section } from '../../components';
import { useBackend } from '../../backend';
import { Button, LabeledList, NumberInput, Section } from '../../components';
import { getGasLabel } from '../../constants';
export const Vent = (props, context) => {
const { vent } = props;
const { act } = useBackend(context);
@@ -26,7 +26,7 @@ export const InterfaceLockNoticeBox = (props, context) => {
// For silicon users
if (siliconUser) {
return (
<NoticeBox color={siliconUser && 'grey'}>
<NoticeBox color="grey">
<Flex align="center">
<Flex.Item>
Interface lock status:
+4 -6
View File
@@ -63,13 +63,11 @@ export const NtosWindow = (props, context) => {
src={resolveAsset(PC_ntneticon)} />
)}
</Box>
{!!PC_showbatteryicon && PC_batteryicon && (
{!!(PC_showbatteryicon && PC_batteryicon) && (
<Box inline mr={1}>
{PC_batteryicon && (
<img
className="NtosHeader__icon"
src={resolveAsset(PC_batteryicon)} />
)}
<img
className="NtosHeader__icon"
src={resolveAsset(PC_batteryicon)} />
{PC_batterypercent && (
PC_batterypercent
)}
+5 -2
View File
@@ -44,6 +44,7 @@ export class Window extends Component {
render() {
const {
resizable,
noClose,
theme,
title,
children,
@@ -74,7 +75,8 @@ export class Window extends Component {
onClose={() => {
logger.log('pressed close');
dispatch(backendSuspendStart());
}} />
}}
noClose={noClose} />
<div
className={classes([
'Window__rest',
@@ -142,6 +144,7 @@ const TitleBar = (props, context) => {
className,
title,
status,
noClose,
fancy,
onDragStart,
onClose,
@@ -180,7 +183,7 @@ const TitleBar = (props, context) => {
<Icon name="bug" />
</div>
)}
{!!fancy && (
{!!fancy && !noClose && (
<div
className="TitleBar__close TitleBar__clickable"
// IE8: Synthetic onClick event doesn't work on IE8.
+1 -1
View File
@@ -7,7 +7,7 @@
"dompurify": "^2.0.12",
"inferno": "^7.4.2",
"inferno-vnode-flags": "^7.4.2",
"marked": "^1.1.0",
"marked": "^1.1.1",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+33
View File
@@ -0,0 +1,33 @@
/**
* Uses DOMPurify to purify/sanitise HTML.
*/
import DOMPurify from 'dompurify';
// Default values
let defTag = [
'br', 'code', 'li', 'p', 'pre',
'span', 'table', 'td', 'tr', 'i',
'th', 'ul', 'ol', 'menu', 'font', 'b',
'center', 'table', 'tr', 'th', 'hr',
];
let defAttr = ['class', 'style'];
/**
* Feed it a string and it should spit out a sanitized version.
*
* @param {string} input
* @param {array} tags
* @param {array} forbidAttr
*/
export const sanitizeText = (input, tags = defTag, forbidAttr = defAttr) => {
// This is VERY important to think first if you NEED
// the tag you put in here. We are pushing all this
// though dangerouslySetInnerHTML and even though
// the default DOMPurify kills javascript, it dosn't
// kill href links or such
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: tags,
FORBID_ATTR: forbidAttr,
});
};
@@ -76,6 +76,13 @@ $bg-map: colors.$bg-map !default;
}
}
.Button--hasContent.Button--iconPosition--right {
.fa, .fas, .far {
margin-right: 0px;
margin-left: 3px;
}
}
.Button--ellipsis {
overflow: hidden;
text-overflow: ellipsis;
@@ -0,0 +1,26 @@
/**
* @file
* @copyright 2020
* @author ThePotato97 (https://github.com/ThePotato97)
* @license ISC
*/
.IconStack > .Icon {
position: absolute;
width: 100%;
text-align: center;
}
.IconStack {
position: relative;
display: inline-block;
height: 1.2em;
line-height: 2em;
vertical-align: middle;
&:after {
color: transparent;
content: '.';
}
}
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2020 bobbahbrown (https://github.com/bobbahbrown)
* SPDX-License-Identifier: MIT
*/
@use '../base.scss';
@use '../colors.scss';
@use '../functions.scss' as *;
$fg-map: colors.$fg-map !default;
$ring-color: #6a96c9 !default;
.RoundGauge {
font-size: 1rem;
width: 2.6em;
height: 1.3em;
margin: 0 auto;
margin-bottom: 0.2em;
}
$pi: 3.1416;
.RoundGauge__ringTrack {
fill: transparent;
stroke: rgba(255, 255, 255, 0.1);
stroke-width: 10;
stroke-dasharray: 50 * $pi;
stroke-dashoffset: 50 * $pi;
}
.RoundGauge__ringFill {
fill: transparent;
stroke: $ring-color;
stroke-width: 10;
stroke-dasharray: 100 * $pi;
transition: stroke 50ms;
}
.RoundGauge__needle, .RoundGauge__ringFill {
transition: transform 50ms ease-in-out;
}
.RoundGauge__needleLine, .RoundGauge__needleMiddle {
fill: colors.$bad;
}
.RoundGauge__alert {
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linejoin: round;
stroke-miterlimit: 2;
fill: rgba(255, 255, 255, 0.1);
}
.RoundGauge__alert.max {
fill: colors.$bad;
}
@each $color-name, $color-value in $fg-map {
.RoundGauge--color--#{$color-name}.RoundGauge__ringFill {
stroke: $color-value;
}
}
@each $color-name, $color-value in $fg-map {
.RoundGauge__alert--#{$color-name} {
fill: $color-value;
transition: opacity 0.6s cubic-bezier(0.25, 1, 0.5, 1);
animation: RoundGauge__alertAnim 1s cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}
}
@keyframes RoundGauge__alertAnim {
0% {
opacity: 0.1;
}
50% {
opacity: 1;
}
100% {
opacity: 0.1;
}
}
@@ -57,6 +57,12 @@ $shadow-offset: 0 0 !default;
height: 100%;
}
.Section--scrollable .Section__content {
overflow-y: scroll;
overflow-x: hidden;
}
.Section--fill .Section__content {
flex-grow: 1;
}
@@ -76,7 +82,7 @@ $shadow-offset: 0 0 !default;
.Section--scrollable {
overflow-x: hidden;
overflow-y: scroll;
overflow-y: hidden;
}
.Section--level--1 .Section__titleText {
@@ -21,7 +21,7 @@ $border-radius: base.$border-radius !default;
&::after {
position: absolute;
display: block;
white-space: nowrap;
white-space: pre;
z-index: 2;
padding: 0.5em 0.75em;
transform: translateX(-50%);
@@ -61,6 +61,26 @@ $border-radius: base.$border-radius !default;
transform: translateX(-50%) translateY(-0.5em);
}
.Tooltip--top-left::after {
bottom: 100%;
right: 50%;
transform: translateX(12px) translateY(8px);
}
.Tooltip--top-left:hover::after {
transform: translateX(12px) translateY(-8px);
}
.Tooltip--top-right::after {
top: 0px;
right: 0px;
transform: translateX(100%) translateY(-50%);
}
.Tooltip--top-right:hover::after {
transform: translateX(100%) translateY(-100%);
}
.Tooltip--bottom::after {
top: 100%;
left: 50%;
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2020 bobbahbrown (https://github.com/bobbahbrown)
* SPDX-License-Identifier: MIT
*/
@use '../colors.scss';
.AlertModal__Message {
text-align: center;
justify-content: center;
}
.AlertModal__Buttons {
justify-content: center;
}
.AlertModal__Loader {
width: 100%;
position: relative;
height: 4px;
}
.AlertModal__LoaderProgress {
position: absolute;
transition: background-color 500ms, width 500ms;
background-color: colors.bg(colors.$primary);
height: 100%;
}
@@ -0,0 +1,18 @@
/**
* @file
* @copyright 2020
* @author ThePotato97 (https://github.com/ThePotato97)
* @license ISC
*/
.Paper {
&__Stamp {
position: absolute;
pointer-events: none;
user-select: none;
}
&__Page {
word-break: break-word;
word-wrap: break-word;
}
}
@@ -0,0 +1,88 @@
.Safe__engraving {
position: absolute;
width: 95%;
height: 96%;
left: 2.5%;
top: 2%;
border: 5px outset #3e4f6a;
padding: 5px;
text-align: center;
}
.Safe__engraving-arrow {
color: #35435a;
}
.Safe__engraving-hinge {
content: " ";
background-color: #191f2a;
width: 25px;
height: 40px;
position: absolute;
right: -15px;
margin-top: -20px;
}
.Safe__dialer {
margin-bottom: 1.25rem;
.Button {
width: 80px;
}
}
.Safe__dialer-right {
.Button {
i {
z-index: -100;
}
}
}
.Safe__dialer-number {
color: #bbbbbb;
display: inline;
background-color: #191f2a;
font-size: 1.5rem;
font-weight: bold;
padding: 0 0.5rem;
}
.Safe__contents {
border: 10px solid #191f2a;
background-color: #0f131a;
height: calc(85% + 7.5px);
text-align: left;
padding: 5px;
}
.Safe__help {
position: absolute;
top: 73%;
left: 10px;
width: 50%;
font-family: 'Comic Sans MS', cursive, sans-serif;
font-style: italic;
color: black;
box-shadow: 5px 5px #111111;
background-image: linear-gradient(to bottom,
#b2ae74 0%,
#8e8b5d 100%);
transform: rotate(-1deg);
&:before {
content: " ";
display: block;
width: 24px;
height: 40px;
background-image: linear-gradient(to bottom,
transparent 0%,
#ffffff 100%);
box-shadow: 1px 1px #111111;
opacity: 0.2;
position: absolute;
top: -30px;
left: calc(50% - 12px);
transform: rotate(-5deg);
}
}
+5
View File
@@ -24,6 +24,7 @@
@include meta.load-css('./components/Divider.scss');
@include meta.load-css('./components/Dropdown.scss');
@include meta.load-css('./components/Flex.scss');
@include meta.load-css('./components/Icon.scss');
@include meta.load-css('./components/Input.scss');
@include meta.load-css('./components/Knob.scss');
@include meta.load-css('./components/LabeledList.scss');
@@ -31,6 +32,7 @@
@include meta.load-css('./components/NoticeBox.scss');
@include meta.load-css('./components/NumberInput.scss');
@include meta.load-css('./components/ProgressBar.scss');
@include meta.load-css('./components/RoundGauge.scss');
@include meta.load-css('./components/Section.scss');
@include meta.load-css('./components/Slider.scss');
@include meta.load-css('./components/Table.scss');
@@ -39,9 +41,12 @@
@include meta.load-css('./components/Tooltip.scss');
// Interfaces
@include meta.load-css('./interfaces/AlertModal.scss');
@include meta.load-css('./interfaces/CameraConsole.scss');
@include meta.load-css('./interfaces/NuclearBomb.scss');
@include meta.load-css('./interfaces/Paper.scss');
@include meta.load-css('./interfaces/Roulette.scss');
@include meta.load-css('./interfaces/Safe.scss');
// Layouts
@include meta.load-css('./layouts/Layout.scss');

Some files were not shown because too many files have changed in this diff Show More