Delete tgui/packages/tgui_ch (#8006)

This commit is contained in:
Kashargul
2024-03-21 13:51:43 +01:00
committed by GitHub
parent 24540d7dfd
commit 42f82df8f2
442 changed files with 6 additions and 58744 deletions

View File

@@ -14,6 +14,3 @@
**.woff2
**.eot
**.ttf
# CHOMPEdit - Until removed
/packages/tgui_ch/**

View File

@@ -18,6 +18,3 @@
## Build artifacts
/public/.tmp/**/*
/public/*.map
## CHOMPEdit - Until removed
/packages/tgui_ch

View File

@@ -1,45 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { Action, AnyAction, Middleware } from '../common/redux';
import { Dispatch } from 'common/redux';
const EXCLUDED_PATTERNS = [/v4shim/i];
const loadedMappings: Record<string, string> = {};
export const resolveAsset = (name: string): string =>
loadedMappings[name] || name;
export const assetMiddleware: Middleware =
(storeApi) =>
<ActionType extends Action = AnyAction>(next: Dispatch<ActionType>) =>
(action: ActionType) => {
const { type, payload } = action as AnyAction;
if (type === 'asset/stylesheet') {
Byond.loadCss(payload);
return;
}
if (type === 'asset/mappings') {
for (const name of Object.keys(payload)) {
// Skip anything that matches excluded patterns
if (EXCLUDED_PATTERNS.some((regex) => regex.test(name))) {
continue;
}
const url = payload[name];
const ext = name.split('.').pop();
loadedMappings[name] = url;
if (ext === 'css') {
Byond.loadCss(url);
}
if (ext === 'js') {
Byond.loadJs(url);
}
}
return;
}
next(action);
};

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 425 200" opacity=".33">
<path d="m 178.00399,0.03869 -71.20393,0 a 6.7613422,6.0255495 0 0 0 -6.76134,6.02555 l 0,187.87147 a 6.7613422,6.0255495 0 0 0 6.76134,6.02554 l 53.1072,0 a 6.7613422,6.0255495 0 0 0 6.76135,-6.02554 l 0,-101.544018 72.21628,104.699398 a 6.7613422,6.0255495 0 0 0 5.76015,2.87016 l 73.55487,0 a 6.7613422,6.0255495 0 0 0 6.76135,-6.02554 l 0,-187.87147 a 6.7613422,6.0255495 0 0 0 -6.76135,-6.02555 l -54.71644,0 a 6.7613422,6.0255495 0 0 0 -6.76133,6.02555 l 0,102.61935 L 183.76413,2.90886 a 6.7613422,6.0255495 0 0 0 -5.76014,-2.87017 z" />
<path d="M 4.8446333,22.10875 A 13.412039,12.501842 0 0 1 13.477588,0.03924 l 66.118315,0 a 5.3648158,5.000737 0 0 1 5.364823,5.00073 l 0,79.87931 z" />
<path d="m 420.15535,177.89119 a 13.412038,12.501842 0 0 1 -8.63295,22.06951 l -66.11832,0 a 5.3648152,5.000737 0 0 1 -5.36482,-5.00074 l 0,-79.87931 z" />
</svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,5 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user-secret" class="svg-inline--fa fa-user-secret fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" opacity=".33">
<path fill="currentColor" d="M383.9 308.3l23.9-62.6c4-10.5-3.7-21.7-15-21.7h-58.5c11-18.9 17.8-40.6 17.8-64v-.3c39.2-7.8 64-19.1 64-31.7 0-13.3-27.3-25.1-70.1-33-9.2-32.8-27-65.8-40.6-82.8-9.5-11.9-25.9-15.6-39.5-8.8l-27.6 13.8c-9 4.5-19.6 4.5-28.6 0L182.1 3.4c-13.6-6.8-30-3.1-39.5 8.8-13.5 17-31.4 50-40.6 82.8-42.7 7.9-70 19.7-70 33 0 12.6 24.8 23.9 64 31.7v.3c0 23.4 6.8 45.1 17.8 64H56.3c-11.5 0-19.2 11.7-14.7 22.3l25.8 60.2C27.3 329.8 0 372.7 0 422.4v44.8C0 491.9 20.1 512 44.8 512h358.4c24.7 0 44.8-20.1 44.8-44.8v-44.8c0-48.4-25.8-90.4-64.1-114.1zM176 480l-41.6-192 49.6 32 24 40-32 120zm96 0l-32-120 24-40 49.6-32L272 480zm41.7-298.5c-3.9 11.9-7 24.6-16.5 33.4-10.1 9.3-48 22.4-64-25-2.8-8.4-15.4-8.4-18.3 0-17 50.2-56 32.4-64 25-9.5-8.8-12.7-21.5-16.5-33.4-.8-2.5-6.3-5.7-6.3-5.8v-10.8c28.3 3.6 61 5.8 96 5.8s67.7-2.1 96-5.8v10.8c-.1.1-5.6 3.2-6.4 5.8z"></path>
</svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 289.742" opacity=".33">
<path d="m 93.537677,0 c -18.113125,0 -34.220133,3.11164 -48.323484,9.33437 -13.965092,6.22167 -24.612442,15.07114 -31.940651,26.5471 -7.1899398,11.33789 -10.3012266,24.74911 -10.3012266,40.23478 0,10.64662 2.7250026,20.46465 8.1751116,29.45258 5.615277,8.98686 14.038277,17.35204 25.268821,25.09436 11.230544,7.60531 26.507421,15.41835 45.830514,23.43782 19.983748,8.29557 34.848848,15.55471 44.592998,21.77638 9.74414,6.22273 16.7617,12.8585 21.05572,19.90951 4.29404,7.05208 6.44193,15.76408 6.44193,26.13459 0,16.17702 -5.20196,28.48222 -15.60673,36.91682 -10.2396,8.4347 -25.02203,12.6523 -44.345169,12.6523 -14.038171,0 -25.515247,-1.6594 -34.433618,-4.9777 -8.91837,-3.4566 -16.185572,-8.7113 -21.800839,-15.7633 -5.615277,-7.0521 -10.074795,-16.66088 -13.377899,-28.82812 l -24.7731626293945,0 0,56.82632 C 33.856769,286.07601 63.74904,289.74201 89.678383,289.74201 c 16.020027,0 30.719787,-1.3827 44.097337,-4.1479 13.54272,-2.9043 25.1041,-7.4676 34.68309,-13.6893 9.74413,-6.3597 17.34042,-14.5195 22.79052,-24.4748 5.4501,-10.09332 8.17511,-22.39959 8.17511,-36.91682 0,-12.99764 -3.3021,-24.33539 -9.90829,-34.0146 -6.44105,-9.81725 -15.52545,-18.52707 -27.25146,-26.13133 -11.56085,-7.60427 -27.91083,-15.83142 -49.05066,-24.68022 -17.50644,-7.19012 -30.719668,-13.68948 -39.638038,-19.49701 -8.918371,-5.80752 -18.607474,-12.43409 -24.096524,-18.87417 -5.426043,-6.36616 -9.658826,-15.07003 -9.658826,-24.88729 0,-9.26401 2.075414,-17.21345 6.223454,-23.85033 11.098298,-14.39748 41.286638,-1.79507 45.075609,24.34762 4.839392,6.77491 8.84935,16.24729 12.029515,28.4156 l 20.53234,0 0,-55.99967 c -4.47825,-5.92448 -9.95488,-10.63222 -15.90837,-14.37411 1.64055,0.47905 3.19039,1.02376 4.63865,1.64024 6.49861,2.62607 12.16793,7.32747 17.0073,14.10345 4.83939,6.77491 8.84935,16.24567 12.02952,28.41397 0,0 8.48128,-0.12894 8.48978,-0.002 0.41776,6.41494 -1.75339,9.45286 -4.12342,12.56104 -2.4174,3.16978 -5.14486,6.78973 -4.00278,13.0029 1.50786,8.20318 10.18354,10.59642 14.62194,9.31154 -3.31842,-0.49911 -5.31855,-1.74948 -5.31855,-1.74948 0,0 1.87646,0.99868 5.65117,-1.35981 -3.27695,0.95571 -10.70529,-0.79738 -11.80125,-6.76313 -0.95752,-5.20861 0.94654,-7.29514 3.40113,-10.51482 2.45462,-3.21968 5.28426,-6.95831 4.6843,-14.48824 l 0.003,0.002 8.92676,0 0,-55.99967 c -15.07125,-3.87168 -27.65314,-6.36042 -37.74671,-7.46586 -9.95531,-1.10755 -20.18823,-1.65981 -30.696613,-1.65981 z m 70.321603,17.30893 0.23805,40.3049 c 1.31808,1.22666 2.43965,2.27815 3.34081,3.10602 4.83939,6.77491 8.84934,16.24566 12.02951,28.41397 l 20.53234,0 0,-55.99967 c -6.67731,-4.59381 -19.83643,-10.47309 -36.14071,-15.82522 z m -28.12049,5.60551 8.56479,17.71655 c -11.97037,-6.46697 -13.84678,-9.71726 -8.56479,-17.71655 z m 22.79705,0 c 2.7715,7.99929 1.78741,11.24958 -4.49354,17.71655 l 4.49354,-17.71655 z m 15.22195,24.00848 8.56479,17.71655 c -11.97038,-6.46697 -13.84679,-9.71726 -8.56479,-17.71655 z m 22.79704,0 c 2.7715,7.99929 1.78741,11.24958 -4.49354,17.71655 l 4.49354,-17.71655 z m -99.11384,2.20764 8.56479,17.71655 c -11.970382,-6.46697 -13.846782,-9.71726 -8.56479,-17.71655 z m 22.79542,0 c 2.7715,7.99929 1.78741,11.24958 -4.49354,17.71655 l 4.49354,-17.71655 z" />
</svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,3 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="meteor" class="svg-inline--fa fa-meteor fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity=".33"><path fill="currentColor" d="M511.328,20.8027c-11.60759,38.70264-34.30724,111.70173-61.30311,187.70077,6.99893,2.09372,13.4042,4,18.60653,5.59368a16.06158,16.06158,0,0,1,9.49854,22.906c-22.106,42.29635-82.69047,152.795-142.47819,214.40356-.99984,1.09373-1.99969,2.5-2.99954,3.49995A194.83046,194.83046,0,1,1,57.085,179.41009c.99985-1,2.40588-2,3.49947-3,61.59994-59.90549,171.97367-120.40473,214.37343-142.4982a16.058,16.058,0,0,1,22.90274,9.49988c1.59351,5.09368,3.49947,11.5936,5.5929,18.59351C379.34818,35.00565,452.43074,12.30281,491.12794.70921A16.18325,16.18325,0,0,1,511.328,20.8027ZM319.951,320.00207A127.98041,127.98041,0,1,0,191.97061,448.00046,127.97573,127.97573,0,0,0,319.951,320.00207Zm-127.98041-31.9996a31.9951,31.9951,0,1,1-31.9951-31.9996A31.959,31.959,0,0,1,191.97061,288.00247Zm31.9951,79.999a15.99755,15.99755,0,1,1-15.99755-15.9998A16.04975,16.04975,0,0,1,223.96571,368.00147Z"></path></svg>
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,373 +0,0 @@
/**
* This file provides a clear separation layer between backend updates
* and what state our React app sees.
*
* Sometimes backend can response without a "data" field, but our final
* state will still contain previous "data" because we are merging
* the response with already existing state.
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { perf } from 'common/perf';
import { createAction } from 'common/redux';
import { setupDrag } from './drag';
import { globalEvents } from './events';
import { focusMap } from './focus';
import { createLogger } from './logging';
import { resumeRenderer, suspendRenderer } from './renderer';
const logger = createLogger('backend');
export const backendUpdate = createAction('backend/update');
export const backendSetSharedState = createAction('backend/setSharedState');
export const backendSuspendStart = createAction('backend/suspendStart');
export const backendSuspendSuccess = () => ({
type: 'backend/suspendSuccess',
payload: {
timestamp: Date.now(),
},
});
const initialState = {
config: {},
data: {},
shared: {},
// Start as suspended
suspended: Date.now(),
suspending: false,
};
export const backendReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === 'backend/update') {
// Merge config
const config = {
...state.config,
...payload.config,
};
// Merge data
const data = {
...state.data,
...payload.static_data,
...payload.data,
};
// Merge shared states
const shared = { ...state.shared };
if (payload.shared) {
for (let key of Object.keys(payload.shared)) {
const value = payload.shared[key];
if (value === '') {
shared[key] = undefined;
} else {
shared[key] = JSON.parse(value);
}
}
}
// Return new state
return {
...state,
config,
data,
shared,
suspended: false,
};
}
if (type === 'backend/setSharedState') {
const { key, nextState } = payload;
return {
...state,
shared: {
...state.shared,
[key]: nextState,
},
};
}
if (type === 'byond/ctrldown') {
globalEvents.emit('byond/ctrldown');
}
if (type === 'byond/ctrlup') {
globalEvents.emit('byond/ctrlup');
}
if (type === 'backend/suspendStart') {
return {
...state,
suspending: true,
};
}
if (type === 'backend/suspendSuccess') {
const { timestamp } = payload;
return {
...state,
data: {},
shared: {},
config: {
...state.config,
title: '',
status: 1,
},
suspending: false,
suspended: timestamp,
};
}
return state;
};
export const backendMiddleware = (store) => {
let fancyState;
let suspendInterval;
return (next) => (action) => {
const { suspended } = selectBackend(store.getState());
const { type, payload } = action;
if (type === 'update') {
store.dispatch(backendUpdate(payload));
return;
}
if (type === 'suspend') {
store.dispatch(backendSuspendSuccess());
return;
}
if (type === 'ping') {
Byond.sendMessage('ping/reply');
return;
}
if (type === 'byond/mousedown') {
globalEvents.emit('byond/mousedown');
}
if (type === 'byond/mouseup') {
globalEvents.emit('byond/mouseup');
}
if (type === 'backend/suspendStart' && !suspendInterval) {
logger.log(`suspending (${Byond.windowId})`);
// Keep sending suspend messages until it succeeds.
// It may fail multiple times due to topic rate limiting.
const suspendFn = () => Byond.sendMessage('suspend');
suspendFn();
suspendInterval = setInterval(suspendFn, 2000);
}
if (type === 'backend/suspendSuccess') {
suspendRenderer();
clearInterval(suspendInterval);
suspendInterval = undefined;
Byond.winset(Byond.windowId, {
'is-visible': false,
});
setImmediate(() => focusMap());
}
if (type === 'backend/update') {
const fancy = payload.config?.window?.fancy;
// Initialize fancy state
if (fancyState === undefined) {
fancyState = fancy;
}
// React to changes in fancy
else if (fancyState !== fancy) {
logger.log('changing fancy mode to', fancy);
fancyState = fancy;
Byond.winset(Byond.windowId, {
titlebar: !fancy,
'can-resize': !fancy,
});
}
}
// Resume on incoming update
if (type === 'backend/update' && suspended) {
// Show the payload
logger.log('backend/update', payload);
// Signal renderer that we have resumed
resumeRenderer();
// Setup drag
setupDrag();
// We schedule this for the next tick here because resizing and unhiding
// during the same tick will flash with a white background.
setImmediate(() => {
perf.mark('resume/start');
// Doublecheck if we are not re-suspended.
const { suspended } = selectBackend(store.getState());
if (suspended) {
return;
}
Byond.winset(Byond.windowId, {
'is-visible': true,
});
perf.mark('resume/finish');
if (process.env.NODE_ENV !== 'production') {
logger.log(
'visible in',
perf.measure('render/finish', 'resume/finish')
);
}
});
}
return next(action);
};
};
/**
* Sends an action to `ui_act` on `src_object` that this tgui window
* is associated with.
*/
export const sendAct = (action: string, payload: object = {}) => {
// Validate that payload is an object
// prettier-ignore
const isObject = typeof payload === 'object'
&& payload !== null
&& !Array.isArray(payload);
if (!isObject) {
logger.error(`Payload for act() must be an object, got this:`, payload);
return;
}
Byond.sendMessage('act/' + action, payload);
};
type BackendState<TData> = {
config: {
title: string;
status: number;
interface: string;
refreshing: boolean;
window: {
key: string;
size: [number, number];
fancy: boolean;
locked: boolean;
};
client: {
ckey: string;
address: string;
computer_id: string;
};
user: {
name: string;
observer: number;
};
};
data: TData;
shared: Record<string, any>;
suspending: boolean;
suspended: boolean;
};
/**
* Selects a backend-related slice of Redux state
*/
export const selectBackend = <TData>(state: any): BackendState<TData> =>
state.backend || {};
/**
* Get data from tgui backend.
*
* Includes the `act` function for performing DM actions.
*/
export const useBackend = <TData>(context: any) => {
const { store } = context;
const state = selectBackend<TData>(store.getState());
return {
...state,
act: sendAct,
};
};
/**
* A tuple that contains the state and a setter function for it.
*/
type StateWithSetter<T> = [T, (nextState: T) => void];
/**
* Allocates state on Redux store without sharing it with other clients.
*
* Use it when you want to have a stateful variable in your component
* that persists between renders, but will be forgotten after you close
* the UI.
*
* It is a lot more performant than `setSharedState`.
*
* @param context React context.
* @param key Key which uniquely identifies this state in Redux store.
* @param initialState Initializes your global variable with this value.
*/
export const useLocalState = <T>(
context: any,
key: string,
initialState: T
): StateWithSetter<T> => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = key in sharedStates ? sharedStates[key] : initialState;
return [
sharedState,
(nextState) => {
store.dispatch(
backendSetSharedState({
key,
nextState:
typeof nextState === 'function'
? nextState(sharedState)
: nextState,
})
);
},
];
};
/**
* Allocates state on Redux store, and **shares** it with other clients
* in the game.
*
* Use it when you want to have a stateful variable in your component
* that persists not only between renders, but also gets pushed to other
* clients that observe this UI.
*
* This makes creation of observable s
*
* @param context React context.
* @param key Key which uniquely identifies this state in Redux store.
* @param initialState Initializes your global variable with this value.
*/
export const useSharedState = <T>(
context: any,
key: string,
initialState: T
): StateWithSetter<T> => {
const { store } = context;
const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = key in sharedStates ? sharedStates[key] : initialState;
return [
sharedState,
(nextState) => {
// prettier-ignore
Byond.sendMessage({
type: 'setSharedState',
key,
value: JSON.stringify(
typeof nextState === 'function'
? nextState(sharedState)
: nextState
) || '',
});
},
];
};

View File

@@ -1,189 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { clamp, toFixed } from 'common/math';
import { Component, createRef } from 'inferno';
const isSafeNumber = (value: number) => {
// prettier-ignore
return typeof value === 'number'
&& Number.isFinite(value)
&& !Number.isNaN(value);
};
export type AnimatedNumberProps = {
/**
* The target value to approach.
*/
value: number;
/**
* If provided, the initial value displayed. By default, the same as `value`.
* If `initial` and `value` are different, the component immediately starts
* animating.
*/
initial?: number;
/**
* If provided, a function that formats the inner string. By default,
* attempts to match the numeric precision of `value`.
*/
format?: (value: number) => string;
};
/**
* Animated numbers are animated at roughly 60 frames per second.
*/
const SIXTY_HZ = 1_000.0 / 60.0;
/**
* The exponential moving average coefficient. Larger values result in a faster
* convergence.
*/
const Q = 0.8333;
/**
* A small number.
*/
const EPSILON = 10e-4;
/**
* An animated number label. Shows a number, formatted with an optionally
* provided function, and animates it towards its target value.
*/
export class AnimatedNumber extends Component<AnimatedNumberProps> {
/**
* The inner `<span/>` being updated sixty times per second.
*/
ref = createRef<HTMLSpanElement>();
/**
* The interval being used to update the inner span.
*/
interval?: NodeJS.Timeout;
/**
* The current value. This values approaches the target value.
*/
currentValue: number = 0;
constructor(props: AnimatedNumberProps) {
super(props);
const { initial, value } = props;
if (initial !== undefined && isSafeNumber(initial)) {
this.currentValue = initial;
} else if (isSafeNumber(value)) {
this.currentValue = value;
}
}
componentDidMount() {
if (this.currentValue !== this.props.value) {
this.startTicking();
}
}
componentWillUnmount() {
// Stop animating when the component is unmounted.
this.stopTicking();
}
shouldComponentUpdate(newProps: AnimatedNumberProps) {
if (newProps.value !== this.props.value) {
// The target value has been adjusted; start animating if we aren't
// already.
this.startTicking();
}
// We render the inner `span` directly using a ref to bypass inferno diffing
// and reach 60 frames per second--tell inferno not to re-render this tree.
return false;
}
/**
* Starts animating the inner span. If the inner span is already animating,
* this is a no-op.
*/
startTicking() {
if (this.interval !== undefined) {
// We're already ticking; do nothing.
return;
}
this.interval = setInterval(() => this.tick(), SIXTY_HZ);
}
/**
* Stops animating the inner span.
*/
stopTicking() {
if (this.interval === undefined) {
// We're not ticking; do nothing.
return;
}
clearInterval(this.interval);
this.interval = undefined;
}
/**
* Steps forward one frame.
*/
tick() {
const { currentValue } = this;
const { value } = this.props;
if (isSafeNumber(value)) {
// Converge towards the value.
this.currentValue = currentValue * Q + value * (1 - Q);
} else {
// If the value is unsafe, we're never going to converge, so stop ticking.
this.stopTicking();
}
if (
Math.abs(value - this.currentValue) < Math.max(EPSILON, EPSILON * value)
) {
// We're about as close as we're going to get--snap to the value and
// stop ticking.
this.currentValue = value;
this.stopTicking();
}
if (this.ref.current) {
// Directly update the inner span, without bothering inferno.
this.ref.current.textContent = this.getText();
}
}
/**
* Gets the inner text of the span.
*/
getText() {
const { props, currentValue } = this;
const { format, value } = props;
if (!isSafeNumber(value)) {
return String(value);
}
if (format) {
return format(this.currentValue);
}
const fraction = String(value).split('.')[1];
const precision = fraction ? fraction.length : 0;
return toFixed(currentValue, clamp(precision, 0, 8));
}
render() {
return <span ref={this.ref}>{this.getText()}</span>;
}
}

View File

@@ -1,19 +0,0 @@
import { Component, createRef } from 'inferno';
export class Autofocus extends Component {
ref = createRef<HTMLDivElement>();
componentDidMount() {
setTimeout(() => {
this.ref.current?.focus();
}, 1);
}
render() {
return (
<div ref={this.ref} tabIndex={-1}>
{this.props.children}
</div>
);
}
}

View File

@@ -1,68 +0,0 @@
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>
);
}
}

View File

@@ -1,13 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { Box } from './Box';
export const BlockQuote = (props) => {
const { className, ...rest } = props;
return <Box className={classes(['BlockQuote', className])} {...rest} />;
};

View File

@@ -1,153 +0,0 @@
import { Component, createRef } from 'inferno';
import { resolveAsset } from '../assets';
import { Box } from './Box';
export enum BodyZone {
Head = 'head',
Chest = 'chest',
LeftArm = 'l_arm',
RightArm = 'r_arm',
LeftLeg = 'l_leg',
RightLeg = 'r_leg',
Eyes = 'eyes',
Mouth = 'mouth',
Groin = 'groin',
}
const bodyZonePixelToZone = (x: number, y: number): BodyZone | null => {
// TypeScript translation of /atom/movable/screen/zone_sel/proc/get_zone_at
if (y < 1) {
return null;
} else if (y < 10) {
if (x > 10 && x < 15) {
return BodyZone.RightLeg;
} else if (x > 17 && x < 22) {
return BodyZone.LeftLeg;
}
} else if (y < 13) {
if (x > 8 && x < 11) {
return BodyZone.RightArm;
} else if (x > 12 && x < 20) {
return BodyZone.Groin;
} else if (x > 21 && x < 24) {
return BodyZone.LeftArm;
}
} else if (y < 22) {
if (x > 8 && x < 11) {
return BodyZone.RightArm;
} else if (x > 12 && x < 20) {
return BodyZone.Chest;
} else if (x > 21 && x < 24) {
return BodyZone.LeftArm;
}
} else if (y < 30 && x > 12 && x < 20) {
if (y > 23 && y < 24 && x > 15 && x < 17) {
return BodyZone.Mouth;
} else if (y > 25 && y < 27 && x > 14 && x < 18) {
return BodyZone.Eyes;
} else {
return BodyZone.Head;
}
}
return null;
};
type BodyZoneSelectorProps = {
onClick?: (zone: BodyZone) => void;
scale?: number;
selectedZone: BodyZone | null;
theme?: string;
};
type BodyZoneSelectorState = {
hoverZone: BodyZone | null;
};
export class BodyZoneSelector extends Component<
BodyZoneSelectorProps,
BodyZoneSelectorState
> {
ref = createRef<HTMLDivElement>();
state: BodyZoneSelectorState = {
hoverZone: null,
};
render() {
const { hoverZone } = this.state;
const { scale = 3, selectedZone, theme = 'midnight' } = this.props;
return (
<div
ref={this.ref}
style={{
width: `${32 * scale}px`,
height: `${32 * scale}px`,
position: 'relative',
}}>
<Box
as="img"
src={resolveAsset(`body_zones.base_${theme}.png`)}
onClick={() => {
const onClick = this.props.onClick;
if (onClick && this.state.hoverZone) {
onClick(this.state.hoverZone);
}
}}
onMouseMove={(event) => {
if (!this.props.onClick) {
return;
}
const rect = this.ref.current?.getBoundingClientRect();
if (!rect) {
return;
}
const x = event.clientX - rect.left;
const y = 32 * scale - (event.clientY - rect.top);
this.setState({
hoverZone: bodyZonePixelToZone(x / scale, y / scale),
});
}}
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
'position': 'absolute',
'width': `${32 * scale}px`,
'height': `${32 * scale}px`,
}}
/>
{selectedZone && (
<Box
as="img"
src={resolveAsset(`body_zones.${selectedZone}.png`)}
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
'pointer-events': 'none',
'position': 'absolute',
'width': `${32 * scale}px`,
'height': `${32 * scale}px`,
}}
/>
)}
{hoverZone && hoverZone !== selectedZone && (
<Box
as="img"
src={resolveAsset(`body_zones.${hoverZone}.png`)}
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
'opacity': 0.5,
'pointer-events': 'none',
'position': 'absolute',
'width': `${32 * scale}px`,
'height': `${32 * scale}px`,
}}
/>
)}
</div>
);
}
}

View File

@@ -1,294 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { createVNode, InfernoNode, SFC } from 'inferno';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { CSS_COLORS } from '../constants';
export type BoxProps = {
[key: string]: any;
as?: string;
className?: string | BooleanLike;
children?: InfernoNode;
position?: string | BooleanLike;
overflow?: string | BooleanLike;
overflowX?: string | BooleanLike;
overflowY?: string | BooleanLike;
top?: string | BooleanLike;
bottom?: string | BooleanLike;
left?: string | BooleanLike;
right?: string | BooleanLike;
width?: string | BooleanLike;
minWidth?: string | BooleanLike;
maxWidth?: string | BooleanLike;
height?: string | BooleanLike;
minHeight?: string | BooleanLike;
maxHeight?: string | BooleanLike;
fontSize?: string | BooleanLike;
fontFamily?: string;
lineHeight?: string | BooleanLike;
opacity?: number;
textAlign?: string | BooleanLike;
verticalAlign?: string | BooleanLike;
textTransform?: string | BooleanLike; // VOREStation Addition
inline?: BooleanLike;
bold?: BooleanLike;
italic?: BooleanLike;
nowrap?: BooleanLike;
preserveWhitespace?: BooleanLike;
m?: string | BooleanLike;
mx?: string | BooleanLike;
my?: string | BooleanLike;
mt?: string | BooleanLike;
mb?: string | BooleanLike;
ml?: string | BooleanLike;
mr?: string | BooleanLike;
p?: string | BooleanLike;
px?: string | BooleanLike;
py?: string | BooleanLike;
pt?: string | BooleanLike;
pb?: string | BooleanLike;
pl?: string | BooleanLike;
pr?: string | BooleanLike;
color?: string | BooleanLike;
textColor?: string | BooleanLike;
backgroundColor?: string | BooleanLike;
// VOREStation Addition Start
// Flex props
flexGrow?: string | BooleanLike;
flexWrap?: string | BooleanLike;
flexBasis?: string | BooleanLike;
flex?: string | BooleanLike;
// VOREStation Addition End
fillPositionedParent?: boolean;
};
/**
* Coverts our rem-like spacing unit into a CSS unit.
*/
export const unit = (value: unknown): string | undefined => {
if (typeof value === 'string') {
// Transparently convert pixels into rem units
if (value.endsWith('px') && !Byond.IS_LTE_IE8) {
return parseFloat(value) / 12 + 'rem';
}
return value;
}
if (typeof value === 'number') {
if (Byond.IS_LTE_IE8) {
return value * 12 + 'px';
}
return value + 'rem';
}
};
/**
* Same as `unit`, but half the size for integers numbers.
*/
export const halfUnit = (value: unknown): string | undefined => {
if (typeof value === 'string') {
return unit(value);
}
if (typeof value === 'number') {
return unit(value * 0.5);
}
};
const isColorCode = (str: unknown) => !isColorClass(str);
const isColorClass = (str: unknown): boolean => {
return typeof str === 'string' && CSS_COLORS.includes(str);
};
const mapRawPropTo = (attrName) => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
style[attrName] = value;
}
};
const mapUnitPropTo = (attrName, unit) => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
style[attrName] = unit(value);
}
};
const mapBooleanPropTo = (attrName, attrValue) => (style, value) => {
if (value) {
style[attrName] = attrValue;
}
};
const mapDirectionalUnitPropTo = (attrName, unit, dirs) => (style, value) => {
if (typeof value === 'number' || typeof value === 'string') {
for (let i = 0; i < dirs.length; i++) {
style[attrName + '-' + dirs[i]] = unit(value);
}
}
};
const mapColorPropTo = (attrName) => (style, value) => {
if (isColorCode(value)) {
style[attrName] = value;
}
};
const styleMapperByPropName = {
// Direct mapping
position: mapRawPropTo('position'),
overflow: mapRawPropTo('overflow'),
overflowX: mapRawPropTo('overflow-x'),
overflowY: mapRawPropTo('overflow-y'),
top: mapUnitPropTo('top', unit),
bottom: mapUnitPropTo('bottom', unit),
left: mapUnitPropTo('left', unit),
right: mapUnitPropTo('right', unit),
width: mapUnitPropTo('width', unit),
minWidth: mapUnitPropTo('min-width', unit),
maxWidth: mapUnitPropTo('max-width', unit),
height: mapUnitPropTo('height', unit),
minHeight: mapUnitPropTo('min-height', unit),
maxHeight: mapUnitPropTo('max-height', unit),
fontSize: mapUnitPropTo('font-size', unit),
fontFamily: mapRawPropTo('font-family'),
lineHeight: (style, value) => {
if (typeof value === 'number') {
style['line-height'] = value;
} else if (typeof value === 'string') {
style['line-height'] = unit(value);
}
},
opacity: mapRawPropTo('opacity'),
textAlign: mapRawPropTo('text-align'),
verticalAlign: mapRawPropTo('vertical-align'),
textTransform: mapRawPropTo('text-transform'), // VOREStation Addition
// Boolean props
inline: mapBooleanPropTo('display', 'inline-block'),
bold: mapBooleanPropTo('font-weight', 'bold'),
italic: mapBooleanPropTo('font-style', 'italic'),
nowrap: mapBooleanPropTo('white-space', 'nowrap'),
preserveWhitespace: mapBooleanPropTo('white-space', 'pre-wrap'),
// Margins
m: mapDirectionalUnitPropTo('margin', halfUnit, [
'top',
'bottom',
'left',
'right',
]),
mx: mapDirectionalUnitPropTo('margin', halfUnit, ['left', 'right']),
my: mapDirectionalUnitPropTo('margin', halfUnit, ['top', 'bottom']),
mt: mapUnitPropTo('margin-top', halfUnit),
mb: mapUnitPropTo('margin-bottom', halfUnit),
ml: mapUnitPropTo('margin-left', halfUnit),
mr: mapUnitPropTo('margin-right', halfUnit),
// Margins
p: mapDirectionalUnitPropTo('padding', halfUnit, [
'top',
'bottom',
'left',
'right',
]),
px: mapDirectionalUnitPropTo('padding', halfUnit, ['left', 'right']),
py: mapDirectionalUnitPropTo('padding', halfUnit, ['top', 'bottom']),
pt: mapUnitPropTo('padding-top', halfUnit),
pb: mapUnitPropTo('padding-bottom', halfUnit),
pl: mapUnitPropTo('padding-left', halfUnit),
pr: mapUnitPropTo('padding-right', halfUnit),
// Color props
color: mapColorPropTo('color'),
textColor: mapColorPropTo('color'),
backgroundColor: mapColorPropTo('background-color'),
// VOREStation Addition Start
// Flex props
flexGrow: mapRawPropTo('flex-grow'),
flexWrap: mapRawPropTo('flex-wrap'),
flexBasis: mapRawPropTo('flex-basis'),
flex: mapRawPropTo('flex'),
// VOREStation Addition End
// Utility props
fillPositionedParent: (style, value) => {
if (value) {
style['position'] = 'absolute';
style['top'] = 0;
style['bottom'] = 0;
style['left'] = 0;
style['right'] = 0;
}
},
};
export const computeBoxProps = (props: BoxProps) => {
const computedProps: HTMLAttributes<any> = {};
const computedStyles = {};
// Compute props
for (let propName of Object.keys(props)) {
if (propName === 'style') {
continue;
}
// IE8: onclick workaround
if (Byond.IS_LTE_IE8 && propName === 'onClick') {
computedProps.onclick = props[propName];
continue;
}
const propValue = props[propName];
const mapPropToStyle = styleMapperByPropName[propName];
if (mapPropToStyle) {
mapPropToStyle(computedStyles, propValue);
} else {
computedProps[propName] = propValue;
}
}
// Concatenate styles
let style = '';
for (let attrName of Object.keys(computedStyles)) {
const attrValue = computedStyles[attrName];
style += attrName + ':' + attrValue + ';';
}
if (props.style) {
for (let attrName of Object.keys(props.style)) {
const attrValue = props.style[attrName];
style += attrName + ':' + attrValue + ';';
}
}
if (style.length > 0) {
computedProps.style = style;
}
return computedProps;
};
export const computeBoxClassName = (props: BoxProps) => {
const color = props.textColor || props.color;
const backgroundColor = props.backgroundColor;
return classes([
isColorClass(color) && 'color-' + color,
isColorClass(backgroundColor) && 'color-bg-' + backgroundColor,
]);
};
export const Box: SFC<BoxProps> = (props: BoxProps) => {
const { as = 'div', className, children, ...rest } = props;
// Render props
if (typeof children === 'function') {
return children(computeBoxProps(props));
}
const computedClassName =
typeof className === 'string'
? className + ' ' + computeBoxClassName(rest)
: computeBoxClassName(rest);
const computedProps = computeBoxProps(rest);
// Render a wrapper element
return createVNode(
VNodeFlags.HtmlElement,
as,
computedClassName,
children,
ChildFlags.UnknownChildren,
computedProps,
undefined
);
};
Box.defaultHooks = pureComponentHooks;

View File

@@ -1,365 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from 'common/keycodes';
import { classes, pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { createLogger } from '../logging';
import { Box, computeBoxClassName, computeBoxProps } from './Box';
import { Icon } from './Icon';
import { Tooltip } from './Tooltip';
const logger = createLogger('Button');
export const Button = (props) => {
const {
className,
fluid,
icon,
iconRotation,
iconSpin,
iconColor,
iconPosition,
iconSize, // VOREStation Addition
color,
disabled,
selected,
tooltip,
tooltipPosition,
ellipsis,
compact,
circular,
content,
children,
onclick,
onClick,
verticalAlignContent,
...rest
} = props;
const hasContent = !!(content || children);
// A warning about the lowercase onclick
if (onclick) {
logger.warn(
`Lowercase 'onclick' is not supported on Button and lowercase` +
` prop names are discouraged in general. Please use a camelCase` +
`'onClick' instead and read: ` +
`https://infernojs.org/docs/guides/event-handling`
);
}
rest.onClick = (e) => {
if (!disabled && onClick) {
onClick(e);
}
};
// IE8: Use "unselectable" because "user-select" doesn't work.
if (Byond.IS_LTE_IE8) {
rest.unselectable = true;
}
let buttonContent = (
<div
className={classes([
'Button',
fluid && 'Button--fluid',
disabled && 'Button--disabled',
selected && 'Button--selected',
hasContent && 'Button--hasContent',
ellipsis && 'Button--ellipsis',
circular && 'Button--circular',
compact && 'Button--compact',
iconPosition && 'Button--iconPosition--' + iconPosition,
verticalAlignContent && 'Button--flex',
verticalAlignContent && fluid && 'Button--flex--fluid',
verticalAlignContent &&
'Button--verticalAlignContent--' + verticalAlignContent,
color && typeof color === 'string'
? 'Button--color--' + color
: 'Button--color--default',
className,
computeBoxClassName(rest),
])}
tabIndex={!disabled && '0'}
onKeyDown={(e) => {
if (props.captureKeys === false) {
return;
}
const keyCode = window.event ? e.which : e.keyCode;
// Simulate a click when pressing space or enter.
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
e.preventDefault();
if (!disabled && onClick) {
onClick(e);
}
return;
}
// Refocus layout on pressing escape.
if (keyCode === KEY_ESCAPE) {
e.preventDefault();
return;
}
}}
{...computeBoxProps(rest)}>
<div className="Button__content">
{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}
fontSize={iconSize} // VOREStation Addition
/>
)}
</div>
</div>
);
if (tooltip) {
buttonContent = (
<Tooltip content={tooltip} position={tooltipPosition}>
{buttonContent}
</Tooltip>
);
}
return buttonContent;
};
Button.defaultHooks = pureComponentHooks;
export const ButtonCheckbox = (props) => {
const { checked, ...rest } = props;
return (
<Button
color="transparent"
icon={checked ? 'check-square-o' : 'square-o'}
selected={checked}
{...rest}
/>
);
};
Button.Checkbox = ButtonCheckbox;
export class ButtonConfirm extends Component {
constructor() {
super();
this.state = {
clickedOnce: false,
};
this.handleClick = () => {
if (this.state.clickedOnce) {
this.setClickedOnce(false);
}
};
}
setClickedOnce(clickedOnce) {
this.setState({
clickedOnce,
});
if (clickedOnce) {
setTimeout(() => window.addEventListener('click', this.handleClick));
} else {
window.removeEventListener('click', this.handleClick);
}
}
render() {
const {
confirmContent = 'Confirm?',
confirmColor = 'bad',
confirmIcon,
icon,
color,
content,
onClick,
...rest
} = this.props;
return (
<Button
content={this.state.clickedOnce ? confirmContent : content}
icon={this.state.clickedOnce ? confirmIcon : icon}
color={this.state.clickedOnce ? confirmColor : color}
onClick={() =>
this.state.clickedOnce ? onClick() : this.setClickedOnce(true)
}
{...rest}
/>
);
}
}
Button.Confirm = ButtonConfirm;
export class ButtonInput extends Component {
constructor() {
super();
this.inputRef = createRef();
this.state = {
inInput: false,
};
}
setInInput(inInput) {
this.setState({
inInput,
});
if (this.inputRef) {
const input = this.inputRef.current;
if (inInput) {
input.value = this.props.currentValue || '';
try {
input.focus();
input.select();
} catch {}
}
}
}
commitResult(e) {
if (this.inputRef) {
const input = this.inputRef.current;
const hasValue = input.value !== '';
if (hasValue) {
this.props.onCommit(e, input.value);
return;
} else {
if (!this.props.defaultValue) {
return;
}
this.props.onCommit(e, this.props.defaultValue);
}
}
}
render() {
const {
fluid,
content,
icon,
iconRotation,
iconSpin,
tooltip,
tooltipPosition,
color = 'default',
placeholder,
maxLength,
...rest
} = this.props;
let buttonContent = (
<Box
className={classes([
'Button',
fluid && 'Button--fluid',
'Button--color--' + color,
])}
{...rest}
onClick={() => this.setInInput(true)}>
{icon && <Icon name={icon} rotation={iconRotation} spin={iconSpin} />}
<div>{content}</div>
<input
ref={this.inputRef}
className="NumberInput__input"
style={{
'display': !this.state.inInput ? 'none' : undefined,
'text-align': 'left',
}}
onBlur={(e) => {
if (!this.state.inInput) {
return;
}
this.setInInput(false);
this.commitResult(e);
}}
onKeyDown={(e) => {
if (e.keyCode === KEY_ENTER) {
this.setInInput(false);
this.commitResult(e);
return;
}
if (e.keyCode === KEY_ESCAPE) {
this.setInInput(false);
}
}}
/>
</Box>
);
if (tooltip) {
buttonContent = (
<Tooltip content={tooltip} position={tooltipPosition}>
{buttonContent}
</Tooltip>
);
}
return buttonContent;
}
}
Button.Input = ButtonInput;
export class ButtonFile extends Component {
constructor() {
super();
this.inputRef = createRef();
}
async read(files) {
const promises = Array.from(files).map((file) => {
let reader = new FileReader();
return new Promise((resolve) => {
reader.onload = () => resolve(reader.result);
reader.readAsText(file);
});
});
return await Promise.all(promises);
}
render() {
const { onSelectFiles, accept, multiple, ...rest } = this.props;
const filePicker = (
<input
hidden
type="file"
ref={this.inputRef}
accept={accept}
multiple={multiple}
onChange={async () => {
const files = this.inputRef.current.files;
if (files.length) {
const readFiles = await this.read(files);
onSelectFiles(multiple ? readFiles : readFiles[0]);
}
}}
/>
);
return (
<>
<Button
{...rest}
onClick={() => {
this.inputRef.current.click();
}}
/>
{filePicker}
</>
);
}
}
Button.File = ButtonFile;

View File

@@ -1,138 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { shallowDiffers } from 'common/react';
import { debounce } from 'common/timer';
import { Component, createRef } from 'inferno';
import { createLogger } from '../logging';
import { computeBoxProps } from './Box';
const logger = createLogger('ByondUi');
// Stack of currently allocated BYOND UI element ids.
const byondUiStack = [];
const createByondUiElement = (elementId) => {
// Reserve an index in the stack
const index = byondUiStack.length;
byondUiStack.push(null);
// Get a unique id
const id = elementId || 'byondui_' + index;
logger.log(`allocated '${id}'`);
// Return a control structure
return {
render: (params) => {
logger.log(`rendering '${id}'`);
byondUiStack[index] = id;
Byond.winset(id, params);
},
unmount: () => {
logger.log(`unmounting '${id}'`);
byondUiStack[index] = null;
Byond.winset(id, {
parent: '',
});
},
};
};
window.addEventListener('beforeunload', () => {
// Cleanly unmount all visible UI elements
for (let index = 0; index < byondUiStack.length; index++) {
const id = byondUiStack[index];
if (typeof id === 'string') {
logger.log(`unmounting '${id}' (beforeunload)`);
byondUiStack[index] = null;
Byond.winset(id, {
parent: '',
});
}
}
});
/**
* Get the bounding box of the DOM element in display-pixels.
*/
const getBoundingBox = (element) => {
const pixelRatio = window.devicePixelRatio ?? 1;
const rect = element.getBoundingClientRect();
// prettier-ignore
return {
pos: [
rect.left * pixelRatio,
rect.top * pixelRatio,
],
size: [
(rect.right - rect.left) * pixelRatio,
(rect.bottom - rect.top) * pixelRatio,
],
};
};
export class ByondUi extends Component {
constructor(props) {
super(props);
this.containerRef = createRef();
this.byondUiElement = createByondUiElement(props.params?.id);
this.handleResize = debounce(() => {
this.forceUpdate();
}, 100);
}
shouldComponentUpdate(nextProps) {
const { params: prevParams = {}, ...prevRest } = this.props;
const { params: nextParams = {}, ...nextRest } = nextProps;
return (
shallowDiffers(prevParams, nextParams) ||
shallowDiffers(prevRest, nextRest)
);
}
componentDidMount() {
// IE8: It probably works, but fuck you anyway.
if (Byond.IS_LTE_IE10) {
return;
}
window.addEventListener('resize', this.handleResize);
this.componentDidUpdate();
this.handleResize();
}
componentDidUpdate() {
// IE8: It probably works, but fuck you anyway.
if (Byond.IS_LTE_IE10) {
return;
}
const { params = {} } = this.props;
const box = getBoundingBox(this.containerRef.current);
logger.debug('bounding box', box);
this.byondUiElement.render({
parent: Byond.windowId,
...params,
pos: box.pos[0] + ',' + box.pos[1],
size: box.size[0] + 'x' + box.size[1],
});
}
componentWillUnmount() {
// IE8: It probably works, but fuck you anyway.
if (Byond.IS_LTE_IE10) {
return;
}
window.removeEventListener('resize', this.handleResize);
this.byondUiElement.unmount();
}
render() {
const { params, ...rest } = this.props;
return (
<div ref={this.containerRef} {...computeBoxProps(rest)}>
{/* Filler */}
<div style={{ 'min-height': '22px' }} />
</div>
);
}
}

View File

@@ -1,127 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { map, zipWith } from 'common/collections';
import { pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { Box } from './Box';
const normalizeData = (data, scale, rangeX, rangeY) => {
if (data.length === 0) {
return [];
}
const min = zipWith(Math.min)(...data);
const max = zipWith(Math.max)(...data);
if (rangeX !== undefined) {
min[0] = rangeX[0];
max[0] = rangeX[1];
}
if (rangeY !== undefined) {
min[1] = rangeY[0];
max[1] = rangeY[1];
}
const normalized = map((point) => {
return zipWith((value, min, max, scale) => {
return ((value - min) / (max - min)) * scale;
})(point, min, max, scale);
})(data);
return normalized;
};
const dataToPolylinePoints = (data) => {
let points = '';
for (let i = 0; i < data.length; i++) {
const point = data[i];
points += point[0] + ',' + point[1] + ' ';
}
return points;
};
class LineChart extends Component {
constructor(props) {
super(props);
this.ref = createRef();
this.state = {
// Initial guess
viewBox: [600, 200],
};
this.handleResize = () => {
const element = this.ref.current;
this.setState({
viewBox: [element.offsetWidth, element.offsetHeight],
});
};
}
componentDidMount() {
window.addEventListener('resize', this.handleResize);
this.handleResize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
render() {
const {
data = [],
rangeX,
rangeY,
fillColor = 'none',
strokeColor = '#ffffff',
strokeWidth = 2,
...rest
} = this.props;
const { viewBox } = this.state;
const normalized = normalizeData(data, viewBox, rangeX, rangeY);
// Push data outside viewBox and form a fillable polygon
if (normalized.length > 0) {
const first = normalized[0];
const last = normalized[normalized.length - 1];
normalized.push([viewBox[0] + strokeWidth, last[1]]);
normalized.push([viewBox[0] + strokeWidth, -strokeWidth]);
normalized.push([-strokeWidth, -strokeWidth]);
normalized.push([-strokeWidth, first[1]]);
}
const points = dataToPolylinePoints(normalized);
return (
<Box position="relative" {...rest}>
{(props) => (
<div ref={this.ref} {...props}>
<svg
viewBox={`0 0 ${viewBox[0]} ${viewBox[1]}`}
preserveAspectRatio="none"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden',
}}>
<polyline
transform={`scale(1, -1) translate(0, -${viewBox[1]})`}
fill={fillColor}
stroke={strokeColor}
strokeWidth={strokeWidth}
points={points}
/>
</svg>
</div>
)}
</Box>
);
}
}
LineChart.defaultHooks = pureComponentHooks;
const Stub = (props) => null;
// IE8: No inline svg support
export const Chart = {
Line: Byond.IS_LTE_IE8 ? Stub : LineChart,
};

View File

@@ -1,45 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { Component } from 'inferno';
import { Box } from './Box';
import { Button } from './Button';
export class Collapsible extends Component {
constructor(props) {
super(props);
const { open } = props;
this.state = {
open: open || false,
};
}
render() {
const { props } = this;
const { open } = this.state;
const { children, color = 'default', title, buttons, ...rest } = props;
return (
<Box mb={1}>
<div className="Table">
<div className="Table__cell">
<Button
fluid
color={color}
icon={open ? 'chevron-down' : 'chevron-right'}
onClick={() => this.setState({ open: !open })}
{...rest}>
{title}
</Button>
</div>
{buttons && (
<div className="Table__cell Table__cell--collapsing">{buttons}</div>
)}
</div>
{open && <Box mt={1}>{children}</Box>}
</Box>
);
}
}

View File

@@ -1,31 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
export const ColorBox = (props) => {
// prettier-ignore
const {
content,
children,
className,
color,
backgroundColor,
...rest
} = props;
rest.color = content ? null : 'transparent';
rest.backgroundColor = color || backgroundColor;
return (
<div
className={classes(['ColorBox', className, computeBoxClassName(rest)])}
{...computeBoxProps(rest)}>
{content || '.'}
</div>
);
};
ColorBox.defaultHooks = pureComponentHooks;

View File

@@ -1,84 +0,0 @@
/**
* @file
* @copyright 2022 raffclar
* @license MIT
*/
import { Box } from './Box';
import { Button } from './Button';
type DialogProps = {
title: any;
onClose: () => void;
children: any;
width?: string;
height?: string;
};
export const Dialog = (props: DialogProps) => {
const { title, onClose, children, width, height } = props;
return (
<div className="Dialog">
<Box className="Dialog__content" width={width || '370px'} height={height}>
<div className="Dialog__header">
<div className="Dialog__title">{title}</div>
<Box mr={2}>
<Button
mr="-3px"
width="26px"
lineHeight="22px"
textAlign="center"
color="transparent"
icon="window-close-o"
tooltip="Close"
tooltipPosition="bottom-start"
onClick={onClose}
/>
</Box>
</div>
{children}
</Box>
</div>
);
};
type DialogButtonProps = {
onClick: () => void;
children: any;
};
const DialogButton = (props: DialogButtonProps) => {
const { onClick, children } = props;
return (
<Button
onClick={onClick}
className="Dialog__button"
verticalAlignContent="middle">
{children}
</Button>
);
};
Dialog.Button = DialogButton;
type UnsavedChangesDialogProps = {
documentName: string;
onSave: () => void;
onDiscard: () => void;
onClose: () => void;
};
export const UnsavedChangesDialog = (props: UnsavedChangesDialogProps) => {
const { documentName, onSave, onDiscard, onClose } = props;
return (
<Dialog title="Notepad" onClose={onClose}>
<div className="Dialog__body">
Do you want to save changes to {documentName}?
</div>
<div className="Dialog__footer">
<DialogButton onClick={onSave}>Save</DialogButton>
<DialogButton onClick={onDiscard}>Don&apos;t Save</DialogButton>
<DialogButton onClick={onClose}>Cancel</DialogButton>
</div>
</Dialog>
);
};

View File

@@ -1,17 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { Box } from './Box';
export const Dimmer = (props) => {
const { className, children, ...rest } = props;
return (
<Box className={classes(['Dimmer', ...className])} {...rest}>
<div className="Dimmer__inner">{children}</div>
</Box>
);
};

View File

@@ -1,20 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
export const Divider = (props) => {
const { vertical, hidden } = props;
return (
<div
className={classes([
'Divider',
hidden && 'Divider--hidden',
vertical ? 'Divider--vertical' : 'Divider--horizontal',
])}
/>
);
};

View File

@@ -1,287 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { clamp } from 'common/math';
import { pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { AnimatedNumber } from './AnimatedNumber';
const DEFAULT_UPDATE_RATE = 400;
/**
* Reduces screen offset to a single number based on the matrix provided.
*/
const getScalarScreenOffset = (e, matrix) => {
return e.screenX * matrix[0] + e.screenY * matrix[1];
};
export class DraggableControl extends Component {
constructor(props) {
super(props);
this.inputRef = createRef();
this.state = {
value: props.value,
dragging: false,
editing: false,
internalValue: null,
origin: null,
suppressingFlicker: false,
};
// Suppresses flickering while the value propagates through the backend
this.flickerTimer = null;
this.suppressFlicker = () => {
const { suppressFlicker } = this.props;
if (suppressFlicker > 0) {
this.setState({
suppressingFlicker: true,
});
clearTimeout(this.flickerTimer);
this.flickerTimer = setTimeout(() => {
this.setState({
suppressingFlicker: false,
});
}, suppressFlicker);
}
};
this.handleDragStart = (e) => {
const { value, dragMatrix } = this.props;
const { editing } = this.state;
if (editing) {
return;
}
document.body.style['pointer-events'] = 'none';
this.ref = e.target;
this.setState({
dragging: false,
origin: getScalarScreenOffset(e, dragMatrix),
value,
internalValue: value,
});
this.timer = setTimeout(() => {
this.setState({
dragging: true,
});
}, 250);
this.dragInterval = setInterval(() => {
const { dragging, value } = this.state;
const { onDrag } = this.props;
if (dragging && onDrag) {
onDrag(e, value);
}
}, this.props.updateRate || DEFAULT_UPDATE_RATE);
document.addEventListener('mousemove', this.handleDragMove);
document.addEventListener('mouseup', this.handleDragEnd);
};
this.handleDragMove = (e) => {
// prettier-ignore
const {
minValue,
maxValue,
step,
stepPixelSize,
dragMatrix,
} = this.props;
this.setState((prevState) => {
const state = { ...prevState };
const offset = getScalarScreenOffset(e, dragMatrix) - state.origin;
if (prevState.dragging) {
const stepOffset = Number.isFinite(minValue) ? minValue % step : 0;
// Translate mouse movement to value
// Give it some headroom (by increasing clamp range by 1 step)
state.internalValue = clamp(
state.internalValue + (offset * step) / stepPixelSize,
minValue - step,
maxValue + step
);
// Clamp the final value
state.value = clamp(
state.internalValue - (state.internalValue % step) + stepOffset,
minValue,
maxValue
);
state.origin = getScalarScreenOffset(e, dragMatrix);
} else if (Math.abs(offset) > 4) {
state.dragging = true;
}
return state;
});
};
this.handleDragEnd = (e) => {
const { onChange, onDrag } = this.props;
const { dragging, value, internalValue } = this.state;
document.body.style['pointer-events'] = 'auto';
clearTimeout(this.timer);
clearInterval(this.dragInterval);
this.setState({
dragging: false,
editing: !dragging,
origin: null,
});
document.removeEventListener('mousemove', this.handleDragMove);
document.removeEventListener('mouseup', this.handleDragEnd);
if (dragging) {
this.suppressFlicker();
if (onChange) {
onChange(e, value);
}
if (onDrag) {
onDrag(e, value);
}
} else if (this.inputRef) {
const input = this.inputRef.current;
input.value = internalValue;
// IE8: Dies when trying to focus a hidden element
// (Error: Object does not support this action)
try {
input.focus();
input.select();
} catch {}
}
};
}
render() {
const {
dragging,
editing,
value: intermediateValue,
suppressingFlicker,
} = this.state;
const {
animated,
value,
unit,
minValue,
maxValue,
unclamped,
format,
onChange,
onDrag,
children,
// Input props
height,
lineHeight,
fontSize,
} = this.props;
let displayValue = value;
if (dragging || suppressingFlicker) {
displayValue = intermediateValue;
}
// prettier-ignore
const displayElement = (
<>
{
(animated && !dragging && !suppressingFlicker) ?
(<AnimatedNumber value={displayValue} format={format} />) :
(format ? format(displayValue) : displayValue)
}
{ (unit ? ' ' + unit : '') }
</>
);
// Setup an input element
// Handles direct input via the keyboard
const inputElement = (
<input
ref={this.inputRef}
className="NumberInput__input"
style={{
display: !editing ? 'none' : undefined,
height: height,
'line-height': lineHeight,
'font-size': fontSize,
}}
onBlur={(e) => {
if (!editing) {
return;
}
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,
});
return;
}
this.setState({
editing: false,
value,
});
this.suppressFlicker();
if (onChange) {
onChange(e, value);
}
if (onDrag) {
onDrag(e, value);
}
}}
onKeyDown={(e) => {
if (e.keyCode === 13) {
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,
});
return;
}
this.setState({
editing: false,
value,
});
this.suppressFlicker();
if (onChange) {
onChange(e, value);
}
if (onDrag) {
onDrag(e, value);
}
return;
}
if (e.keyCode === 27) {
this.setState({
editing: false,
});
return;
}
}}
/>
);
// Return a part of the state for higher-level components to use.
return children({
dragging,
editing,
value,
displayValue,
displayElement,
inputElement,
handleDragStart: this.handleDragStart,
});
}
}
DraggableControl.defaultHooks = pureComponentHooks;
DraggableControl.defaultProps = {
minValue: -Infinity,
maxValue: +Infinity,
step: 1,
stepPixelSize: 1,
suppressFlicker: 50,
dragMatrix: [1, 0],
};

View File

@@ -1,395 +0,0 @@
import { createPopper, VirtualElement } from '@popperjs/core';
import { classes } from 'common/react';
import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';
import { Box, BoxProps } from './Box';
import { Button } from './Button';
import { Icon } from './Icon';
import { Stack } from './Stack';
export interface DropdownEntry {
displayText: string | number | InfernoNode;
value: string | number | Enumerator;
}
type DropdownUniqueProps = {
options: string[] | DropdownEntry[];
icon?: string;
iconRotation?: number;
clipSelectedText?: boolean;
width?: string;
menuWidth?: string;
over?: boolean;
color?: string;
nochevron?: boolean;
displayText?: string | number | InfernoNode;
onClick?: (event) => void;
// you freaks really are just doing anything with this shit
selected?: any;
onSelected?: (selected: any) => void;
buttons?: boolean;
};
export type DropdownProps = BoxProps & DropdownUniqueProps;
const DEFAULT_OPTIONS = {
placement: 'left-start',
modifiers: [
{
name: 'eventListeners',
enabled: false,
},
],
};
const NULL_RECT: DOMRect = {
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
x: 0,
y: 0,
toJSON: () => null,
} as const;
type DropdownState = {
selected?: string;
open: boolean;
};
const DROPDOWN_DEFAULT_CLASSNAMES = 'Layout Dropdown__menu';
const DROPDOWN_SCROLL_CLASSNAMES = 'Layout Dropdown__menu-scroll';
export class Dropdown extends Component<DropdownProps, DropdownState> {
static renderedMenu: HTMLDivElement | undefined;
static singletonPopper: ReturnType<typeof createPopper> | undefined;
static currentOpenMenu: Element | undefined;
static virtualElement: VirtualElement = {
getBoundingClientRect: () =>
Dropdown.currentOpenMenu?.getBoundingClientRect() ?? NULL_RECT,
};
menuContents: any;
state: DropdownState = {
open: false,
selected: this.props.selected,
};
handleClick = () => {
if (this.state.open) {
this.setOpen(false);
}
};
getDOMNode() {
return findDOMfromVNode(this.$LI, true);
}
componentDidMount() {
const domNode = this.getDOMNode();
if (!domNode) {
return;
}
}
openMenu() {
let renderedMenu = Dropdown.renderedMenu;
if (renderedMenu === undefined) {
renderedMenu = document.createElement('div');
renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES;
document.body.appendChild(renderedMenu);
Dropdown.renderedMenu = renderedMenu;
}
const domNode = this.getDOMNode()!;
Dropdown.currentOpenMenu = domNode;
renderedMenu.scrollTop = 0;
renderedMenu.style.width =
this.props.menuWidth ||
// Hack, but domNode should *always* be the parent control meaning it will have width
// @ts-ignore
`${domNode.offsetWidth}px`;
renderedMenu.style.opacity = '1';
renderedMenu.style.pointerEvents = 'auto';
// ie hack
// ie has this bizarre behavior where focus just silently fails if the
// element being targeted "isn't ready"
// 400 is probably way too high, but the lack of hotloading is testing my
// patience on tuning it
// I'm beyond giving a shit at this point it fucking works whatever
setTimeout(() => {
Dropdown.renderedMenu?.focus();
}, 400);
this.renderMenuContent();
}
closeMenu() {
if (Dropdown.currentOpenMenu !== this.getDOMNode()) {
return;
}
Dropdown.currentOpenMenu = undefined;
Dropdown.renderedMenu!.style.opacity = '0';
Dropdown.renderedMenu!.style.pointerEvents = 'none';
}
componentWillUnmount() {
this.closeMenu();
this.setOpen(false);
}
renderMenuContent() {
const renderedMenu = Dropdown.renderedMenu;
if (!renderedMenu) {
return;
}
if (renderedMenu.offsetHeight > 200) {
renderedMenu.className = DROPDOWN_SCROLL_CLASSNAMES;
} else {
renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES;
}
const { options = [] } = this.props;
const ops = options.map((option) => {
let value, displayText;
if (typeof option === 'string') {
displayText = option;
value = option;
} else if (option !== null) {
displayText = option.displayText;
value = option.value;
}
return (
<div
key={value}
className={classes([
'Dropdown__menuentry',
this.state.selected === value && 'selected',
])}
onClick={() => {
this.setSelected(value);
}}>
{displayText}
</div>
);
});
const to_render = ops.length ? ops : 'No Options Found';
render(
<div>{to_render}</div>,
renderedMenu,
() => {
let singletonPopper = Dropdown.singletonPopper;
if (singletonPopper === undefined) {
singletonPopper = createPopper(
Dropdown.virtualElement,
renderedMenu!,
{
...DEFAULT_OPTIONS,
placement: 'bottom-start',
}
);
Dropdown.singletonPopper = singletonPopper;
} else {
singletonPopper.setOptions({
...DEFAULT_OPTIONS,
placement: 'bottom-start',
});
singletonPopper.update();
}
},
this.context
);
}
setOpen(open: boolean) {
this.setState((state) => ({
...state,
open,
}));
if (open) {
setTimeout(() => {
this.openMenu();
window.addEventListener('click', this.handleClick);
});
} else {
this.closeMenu();
window.removeEventListener('click', this.handleClick);
}
}
setSelected(selected: string) {
this.setState((state) => ({
...state,
selected,
}));
this.setOpen(false);
if (this.props.onSelected) {
this.props.onSelected(selected);
}
}
getOptionValue(option): string {
return typeof option === 'string' ? option : option.value;
}
getSelectedIndex(): number {
const selected = this.state.selected || this.props.selected;
const { options = [] } = this.props;
return options.findIndex((option) => {
return this.getOptionValue(option) === selected;
});
}
toPrevious(): void {
if (this.props.options.length < 1) {
return;
}
let selectedIndex = this.getSelectedIndex();
const startIndex = 0;
const endIndex = this.props.options.length - 1;
const hasSelected = selectedIndex >= 0;
if (!hasSelected) {
selectedIndex = startIndex;
}
const previousIndex =
selectedIndex === startIndex ? endIndex : selectedIndex - 1;
this.setSelected(this.getOptionValue(this.props.options[previousIndex]));
}
toNext(): void {
if (this.props.options.length < 1) {
return;
}
let selectedIndex = this.getSelectedIndex();
const startIndex = 0;
const endIndex = this.props.options.length - 1;
const hasSelected = selectedIndex >= 0;
if (!hasSelected) {
selectedIndex = endIndex;
}
const nextIndex =
selectedIndex === endIndex ? startIndex : selectedIndex + 1;
this.setSelected(this.getOptionValue(this.props.options[nextIndex]));
}
render() {
const { props } = this;
const {
icon,
iconRotation,
iconSpin,
clipSelectedText = true,
color = 'default',
dropdownStyle,
over,
nochevron,
width,
onClick,
onSelected,
selected,
disabled,
displayText,
buttons,
...boxProps
} = props;
const { className, ...rest } = boxProps;
const adjustedOpen = over ? !this.state.open : this.state.open;
return (
<Stack fill>
<Stack.Item width={width}>
<Box
width={'100%'}
className={classes([
'Dropdown__control',
'Button',
'Button--color--' + color,
disabled && 'Button--disabled',
className,
])}
onClick={(event) => {
if (disabled && !this.state.open) {
return;
}
this.setOpen(!this.state.open);
if (onClick) {
onClick(event);
}
}}
{...rest}>
{icon && (
<Icon
name={icon}
rotation={iconRotation}
spin={iconSpin}
mr={1}
/>
)}
<span
className="Dropdown__selected-text"
style={{
overflow: clipSelectedText ? 'hidden' : 'visible',
}}>
{displayText || this.state.selected}
</span>
{nochevron || (
<span className="Dropdown__arrow-button">
<Icon name={adjustedOpen ? 'chevron-up' : 'chevron-down'} />
</span>
)}
</Box>
</Stack.Item>
{buttons && (
<>
<Stack.Item height={'100%'}>
<Button
height={'100%'}
icon="chevron-left"
disabled={disabled}
onClick={() => {
if (disabled) {
return;
}
this.toPrevious();
}}
/>
</Stack.Item>
<Stack.Item height={'100%'}>
<Button
height={'100%'}
icon="chevron-right"
disabled={disabled}
onClick={() => {
if (disabled) {
return;
}
this.toNext();
}}
/>
</Stack.Item>
</>
)}
</Stack>
);
}
}

View File

@@ -1,51 +0,0 @@
import { Box } from './Box';
import { Component, Fragment } from 'inferno';
export class FakeTerminal extends Component {
constructor(props) {
super(props);
this.timer = null;
this.state = {
currentIndex: 0,
currentDisplay: [],
};
}
tick() {
const { props, state } = this;
if (state.currentIndex <= props.allMessages.length) {
this.setState((prevState) => {
return {
currentIndex: prevState.currentIndex + 1,
};
});
const { currentDisplay } = state;
currentDisplay.push(props.allMessages[state.currentIndex]);
} else {
clearTimeout(this.timer);
setTimeout(props.onFinished, props.finishedTimeout);
}
}
componentDidMount() {
const { linesPerSecond = 2.5 } = this.props;
this.timer = setInterval(() => this.tick(), 1000 / linesPerSecond);
}
componentWillUnmount() {
clearTimeout(this.timer);
}
render() {
return (
<Box m={1}>
{this.state.currentDisplay.map((value) => (
<Fragment key={value}>
{value}
<br />
</Fragment>
))}
</Box>
);
}
}

View File

@@ -1,91 +0,0 @@
import { Component, createRef, RefObject } from 'inferno';
const DEFAULT_ACCEPTABLE_DIFFERENCE = 5;
type Props = {
acceptableDifference?: number;
maxWidth: number;
maxFontSize: number;
native?: HTMLAttributes<HTMLDivElement>;
};
type State = {
fontSize: number;
};
export class FitText extends Component<Props, State> {
ref: RefObject<HTMLDivElement> = createRef();
state: State = {
fontSize: 0,
};
constructor() {
super();
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);
}
componentDidUpdate(prevProps) {
if (prevProps.children !== this.props.children) {
this.resize();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
resize() {
const element = this.ref.current;
if (!element) {
return;
}
const maxWidth = this.props.maxWidth;
let start = 0;
let end = this.props.maxFontSize;
for (let _ = 0; _ < 10; _++) {
const middle = Math.round((start + end) / 2);
element.style.fontSize = `${middle}px`;
const difference = element.offsetWidth - maxWidth;
if (difference > 0) {
end = middle;
} else if (
difference <
(this.props.acceptableDifference ?? DEFAULT_ACCEPTABLE_DIFFERENCE)
) {
start = middle;
} else {
break;
}
}
this.setState({
fontSize: Math.round((start + end) / 2),
});
}
componentDidMount() {
this.resize();
}
render() {
return (
<span
ref={this.ref}
style={{
'font-size': `${this.state.fontSize}px`,
...(typeof this.props.native?.style === 'object' &&
this.props.native.style),
}}>
{this.props.children}
</span>
);
}
}

View File

@@ -1,115 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { BoxProps, computeBoxClassName, computeBoxProps, unit } from './Box';
export type FlexProps = BoxProps & {
direction?: string | BooleanLike;
wrap?: string | BooleanLike;
align?: string | BooleanLike;
justify?: string | BooleanLike;
inline?: BooleanLike;
};
export const computeFlexClassName = (props: FlexProps) => {
return classes([
'Flex',
props.inline && 'Flex--inline',
Byond.IS_LTE_IE10 && 'Flex--iefix',
Byond.IS_LTE_IE10 && props.direction === 'column' && 'Flex--iefix--column',
computeBoxClassName(props),
]);
};
export const computeFlexProps = (props: FlexProps) => {
const { className, direction, wrap, align, justify, inline, ...rest } = props;
return computeBoxProps({
style: {
...rest.style,
'flex-direction': direction,
'flex-wrap': wrap === true ? 'wrap' : wrap,
'align-items': align,
'justify-content': justify,
},
...rest,
});
};
export const Flex = (props) => {
const { className, ...rest } = props;
return (
<div
className={classes([className, computeFlexClassName(rest)])}
{...computeFlexProps(rest)}
/>
);
};
Flex.defaultHooks = pureComponentHooks;
export type FlexItemProps = BoxProps & {
grow?: number;
order?: number;
shrink?: number;
basis?: string | BooleanLike;
align?: string | BooleanLike;
};
export const computeFlexItemClassName = (props: FlexItemProps) => {
return classes([
'Flex__item',
Byond.IS_LTE_IE10 && 'Flex__item--iefix',
computeBoxClassName(props),
]);
};
export const computeFlexItemProps = (props: FlexItemProps) => {
// prettier-ignore
const {
className,
style,
grow,
order,
shrink,
basis,
align,
...rest
} = props;
// prettier-ignore
const computedBasis = basis
// IE11: Set basis to specified width if it's known, which fixes certain
// bugs when rendering tables inside the flex.
?? props.width
// If grow is used, basis should be set to 0 to be consistent with
// flex css shorthand `flex: 1`.
?? (grow !== undefined ? 0 : undefined);
return computeBoxProps({
style: {
...style,
'flex-grow': grow !== undefined && Number(grow),
'flex-shrink': shrink !== undefined && Number(shrink),
'flex-basis': unit(computedBasis),
'order': order,
'align-self': align,
},
...rest,
});
};
const FlexItem = (props) => {
const { className, ...rest } = props;
return (
<div
className={classes([className, computeFlexItemClassName(props)])}
{...computeFlexItemProps(rest)}
/>
);
};
FlexItem.defaultHooks = pureComponentHooks;
Flex.Item = FlexItem;

View File

@@ -1,38 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { Table } from './Table';
import { pureComponentHooks } from 'common/react';
/** @deprecated */
export const Grid = (props) => {
const { children, ...rest } = props;
return (
<Table {...rest}>
<Table.Row>{children}</Table.Row>
</Table>
);
};
Grid.defaultHooks = pureComponentHooks;
/** @deprecated */
export const GridColumn = (props) => {
const { size = 1, style, ...rest } = props;
return (
<Table.Cell
style={{
width: size + '%',
...style,
}}
{...rest}
/>
);
};
Grid.defaultHooks = pureComponentHooks;
Grid.Column = GridColumn;

View File

@@ -1,98 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @author Original Aleksej Komarov
* @author Changes ThePotato97
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { InfernoNode } from 'inferno';
import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
const FA_OUTLINE_REGEX = /-o$/;
type IconPropsUnique = {
name: string;
size?: number;
spin?: boolean;
className?: string;
rotation?: number;
style?: string | CSSProperties;
};
export type IconProps = IconPropsUnique & BoxProps;
export const Icon = (props: IconProps) => {
let { style, ...restlet } = props;
const { name, size, spin, className, rotation, ...rest } = restlet;
if (size) {
if (!style) {
style = {};
}
style['font-size'] = size * 100 + '%';
}
if (rotation) {
if (!style) {
style = {};
}
style['transform'] = `rotate(${rotation}deg)`;
}
rest.style = style;
const boxProps = computeBoxProps(rest);
let iconClass = '';
if (name.startsWith('tg-')) {
// tgfont icon
iconClass = name;
} else {
// font awesome icon
const faRegular = FA_OUTLINE_REGEX.test(name);
const faName = name.replace(FA_OUTLINE_REGEX, '');
const preprendFa = !faName.startsWith('fa-');
iconClass = faRegular ? 'far ' : 'fas ';
if (preprendFa) {
iconClass += 'fa-';
}
iconClass += faName;
if (spin) {
iconClass += ' fa-spin';
}
}
return (
<i
className={classes([
'Icon',
iconClass,
className,
computeBoxClassName(rest),
])}
{...boxProps}
/>
);
};
Icon.defaultHooks = pureComponentHooks;
type IconStackUnique = {
children: InfernoNode;
className?: string;
};
export type IconStackProps = IconStackUnique & BoxProps;
export const IconStack = (props: IconStackProps) => {
const { className, children, ...rest } = props;
return (
<span
class={classes(['IconStack', className, computeBoxClassName(rest)])}
{...computeBoxProps(rest)}>
{children}
</span>
);
};
Icon.Stack = IconStack;

View File

@@ -1,188 +0,0 @@
import { computeBoxProps } from './Box';
import { Stack } from './Stack';
import { ProgressBar } from './ProgressBar';
import { Button } from './Button';
import { Component } from 'inferno';
const ZOOM_MIN_VAL = 0.5;
const ZOOM_MAX_VAL = 1.5;
const ZOOM_INCREMENT = 0.1;
export class InfinitePlane extends Component {
constructor() {
super();
this.state = {
mouseDown: false,
left: 0,
top: 0,
lastLeft: 0,
lastTop: 0,
zoom: 1,
};
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleZoomIncrease = this.handleZoomIncrease.bind(this);
this.handleZoomDecrease = this.handleZoomDecrease.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.doOffsetMouse = this.doOffsetMouse.bind(this);
}
componentDidMount() {
window.addEventListener('mouseup', this.onMouseUp);
window.addEventListener('mousedown', this.doOffsetMouse);
window.addEventListener('mousemove', this.doOffsetMouse);
window.addEventListener('mouseup', this.doOffsetMouse);
}
componentWillUnmount() {
window.removeEventListener('mouseup', this.onMouseUp);
window.removeEventListener('mousedown', this.doOffsetMouse);
window.removeEventListener('mousemove', this.doOffsetMouse);
window.removeEventListener('mouseup', this.doOffsetMouse);
}
doOffsetMouse(event) {
const { zoom } = this.state;
event.screenZoomX = event.screenX * Math.pow(zoom, -1);
event.screenZoomY = event.screenY * Math.pow(zoom, -1);
}
handleMouseDown(event) {
this.setState((state) => {
return {
mouseDown: true,
lastLeft: event.clientX - state.left,
lastTop: event.clientY - state.top,
};
});
}
onMouseUp() {
this.setState({
mouseDown: false,
});
}
handleZoomIncrease(event) {
const { onZoomChange } = this.props;
const { zoom } = this.state;
const newZoomValue = Math.min(zoom + ZOOM_INCREMENT, ZOOM_MAX_VAL);
this.setState({
zoom: newZoomValue,
});
if (onZoomChange) {
onZoomChange(newZoomValue);
}
}
handleZoomDecrease(event) {
const { onZoomChange } = this.props;
const { zoom } = this.state;
const newZoomValue = Math.max(zoom - ZOOM_INCREMENT, ZOOM_MIN_VAL);
this.setState({
zoom: newZoomValue,
});
if (onZoomChange) {
onZoomChange(newZoomValue);
}
}
handleMouseMove(event) {
const { onBackgroundMoved, initialLeft = 0, initialTop = 0 } = this.props;
if (this.state.mouseDown) {
let newX, newY;
this.setState((state) => {
newX = event.clientX - state.lastLeft;
newY = event.clientY - state.lastTop;
return {
left: newX,
top: newY,
};
});
if (onBackgroundMoved) {
onBackgroundMoved(newX + initialLeft, newY + initialTop);
}
}
}
render() {
const {
children,
backgroundImage,
imageWidth,
initialLeft = 0,
initialTop = 0,
...rest
} = this.props;
const { left, top, zoom } = this.state;
const finalLeft = initialLeft + left;
const finalTop = initialTop + top;
return (
<div
ref={this.ref}
{...computeBoxProps({
...rest,
style: {
...rest.style,
overflow: 'hidden',
position: 'relative',
},
})}>
<div
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
style={{
'position': 'fixed',
'height': '100%',
'width': '100%',
'background-image': `url("${backgroundImage}")`,
'background-position': `${finalLeft}px ${finalTop}px`,
'background-repeat': 'repeat',
'background-size': `${zoom * imageWidth}px`,
}}
/>
<div
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
style={{
'position': 'fixed',
'transform': `translate(${finalLeft}px, ${finalTop}px) scale(${zoom})`,
'transform-origin': 'top left',
'height': '100%',
'width': '100%',
}}>
{children}
</div>
<Stack position="absolute" width="100%">
<Stack.Item>
<Button icon="minus" onClick={this.handleZoomDecrease} />
</Stack.Item>
<Stack.Item grow={1}>
<ProgressBar
minValue={ZOOM_MIN_VAL}
value={zoom}
maxValue={ZOOM_MAX_VAL}>
{zoom}x
</ProgressBar>
</Stack.Item>
<Stack.Item>
<Button icon="plus" onClick={this.handleZoomIncrease} />
</Stack.Item>
</Stack>
</div>
);
}
}

View File

@@ -1,156 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { KEY_ENTER, KEY_ESCAPE } from 'common/keycodes';
import { classes } from 'common/react';
import { Component, createRef } from 'inferno';
import { Box } from './Box';
// prettier-ignore
export const toInputValue = value => (
typeof value !== 'number' && typeof value !== 'string'
? ''
: String(value)
);
export class Input extends Component {
constructor() {
super();
this.inputRef = createRef();
this.state = {
editing: false,
};
this.handleInput = (e) => {
const { editing } = this.state;
const { onInput } = this.props;
if (!editing) {
this.setEditing(true);
}
if (onInput) {
onInput(e, e.target.value);
}
};
this.handleFocus = (e) => {
const { editing } = this.state;
if (!editing) {
this.setEditing(true);
}
};
this.handleBlur = (e) => {
const { editing } = this.state;
const { onChange } = this.props;
if (editing) {
this.setEditing(false);
if (onChange) {
onChange(e, e.target.value);
}
}
};
this.handleKeyDown = (e) => {
const { onInput, onChange, onEnter } = this.props;
if (e.keyCode === KEY_ENTER) {
this.setEditing(false);
if (onChange) {
onChange(e, e.target.value);
}
if (onInput) {
onInput(e, e.target.value);
}
if (onEnter) {
onEnter(e, e.target.value);
}
if (this.props.selfClear) {
e.target.value = '';
} else {
e.target.blur();
}
return;
}
if (e.keyCode === KEY_ESCAPE) {
if (this.props.onEscape) {
this.props.onEscape(e);
return;
}
this.setEditing(false);
e.target.value = toInputValue(this.props.value);
e.target.blur();
return;
}
};
}
componentDidMount() {
const nextValue = this.props.value;
const input = this.inputRef.current;
if (input) {
input.value = toInputValue(nextValue);
}
if (this.props.autoFocus || this.props.autoSelect) {
setTimeout(() => {
input.focus();
if (this.props.autoSelect) {
input.select();
}
}, 1);
}
}
componentDidUpdate(prevProps, prevState) {
const { editing } = this.state;
const prevValue = prevProps.value;
const nextValue = this.props.value;
const input = this.inputRef.current;
if (input && !editing && prevValue !== nextValue) {
input.value = toInputValue(nextValue);
}
}
setEditing(editing) {
this.setState({ editing });
}
render() {
const { props } = this;
// Input only props
const {
selfClear,
onInput,
onChange,
onEnter,
value,
maxLength,
placeholder,
...boxProps
} = props;
// Box props
const { className, fluid, monospace, ...rest } = boxProps;
return (
<Box
className={classes([
'Input',
fluid && 'Input--fluid',
monospace && 'Input--monospace',
className,
])}
{...rest}>
<div className="Input__baseline">.</div>
<input
ref={this.inputRef}
className="Input__input"
placeholder={placeholder}
onInput={this.handleInput}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
maxLength={maxLength}
/>
</Box>
);
}
}

View File

@@ -1,39 +0,0 @@
import { Component } from 'inferno';
import { KeyEvent } from '../events';
import { listenForKeyEvents } from '../hotkeys';
type KeyListenerProps = Partial<{
onKey: (key: KeyEvent) => void;
onKeyDown: (key: KeyEvent) => void;
onKeyUp: (key: KeyEvent) => void;
}>;
export class KeyListener extends Component<KeyListenerProps> {
dispose: () => void;
constructor() {
super();
this.dispose = listenForKeyEvents((key) => {
if (this.props.onKey) {
this.props.onKey(key);
}
if (key.isDown() && this.props.onKeyDown) {
this.props.onKeyDown(key);
}
if (key.isUp() && this.props.onKeyUp) {
this.props.onKeyUp(key);
}
});
}
componentWillUnmount() {
this.dispose();
}
render() {
return null;
}
}

View File

@@ -1,138 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
import { DraggableControl } from './DraggableControl';
import { NumberInput } from './NumberInput';
export const Knob = (props) => {
// IE8: I don't want to support a yet another component on IE8.
// IE8: It also can't handle SVG.
if (Byond.IS_LTE_IE8) {
return <NumberInput {...props} />;
}
const {
// Draggable props (passthrough)
animated,
format,
maxValue,
minValue,
unclamped,
onChange,
onDrag,
step,
stepPixelSize,
suppressFlicker,
unit,
value,
// Own props
className,
style,
fillValue,
color,
ranges = {},
size = 1,
bipolar,
children,
...rest
} = props;
return (
<DraggableControl
dragMatrix={[0, -1]}
{...{
animated,
format,
maxValue,
minValue,
unclamped,
onChange,
onDrag,
step,
stepPixelSize,
suppressFlicker,
unit,
value,
}}>
{(control) => {
const {
dragging,
editing,
value,
displayValue,
displayElement,
inputElement,
handleDragStart,
} = control;
const scaledFillValue = scale(
fillValue ?? displayValue,
minValue,
maxValue
);
const scaledDisplayValue = scale(displayValue, minValue, maxValue);
const effectiveColor =
color || keyOfMatchingRange(fillValue ?? value, ranges) || 'default';
const rotation = Math.min((scaledDisplayValue - 0.5) * 270, 225);
return (
<div
className={classes([
'Knob',
'Knob--color--' + effectiveColor,
bipolar && 'Knob--bipolar',
className,
computeBoxClassName(rest),
])}
{...computeBoxProps({
style: {
'font-size': size + 'em',
...style,
},
...rest,
})}
onMouseDown={handleDragStart}>
<div className="Knob__circle">
<div
className="Knob__cursorBox"
style={{
transform: `rotate(${rotation}deg)`,
}}>
<div className="Knob__cursor" />
</div>
</div>
{dragging && (
<div className="Knob__popupValue">{displayElement}</div>
)}
<svg
className="Knob__ring Knob__ringTrackPivot"
viewBox="0 0 100 100">
<circle className="Knob__ringTrack" cx="50" cy="50" r="50" />
</svg>
<svg
className="Knob__ring Knob__ringFillPivot"
viewBox="0 0 100 100">
<circle
className="Knob__ringFill"
style={{
'stroke-dashoffset': Math.max(
((bipolar ? 2.75 : 2.0) - scaledFillValue * 1.5) *
Math.PI *
50,
0
),
}}
cx="50"
cy="50"
r="50"
/>
</svg>
{inputElement}
</div>
);
}}
</DraggableControl>
);
};

View File

@@ -1,42 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
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}>
{children}
</Flex>
);
};
const LabeledControlsItem = (props) => {
const { label, children, mx = 1, ...rest } = props;
return (
<Flex.Item mx={mx}>
<Flex
height="100%"
direction="column"
align="center"
textAlign="center"
justify="space-between"
{...rest}>
<Flex.Item />
<Flex.Item>{children}</Flex.Item>
<Flex.Item color="label">{label}</Flex.Item>
</Flex>
</Flex.Item>
);
};
LabeledControls.Item = LabeledControlsItem;

View File

@@ -1,105 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { InfernoNode } from 'inferno';
import { Box, unit } from './Box';
import { Divider } from './Divider';
type LabeledListProps = {
children?: any;
};
export const LabeledList = (props: LabeledListProps) => {
const { children } = props;
return <table className="LabeledList">{children}</table>;
};
LabeledList.defaultHooks = pureComponentHooks;
type LabeledListItemProps = {
className?: string | BooleanLike;
label?: string | InfernoNode | BooleanLike;
labelColor?: string | BooleanLike;
labelWrap?: boolean;
color?: string | BooleanLike;
textAlign?: string | BooleanLike;
buttons?: InfernoNode;
/** @deprecated */
content?: any;
children?: InfernoNode;
verticalAlign?: string;
};
const LabeledListItem = (props: LabeledListItemProps) => {
const {
className,
label,
labelColor = 'label',
labelWrap,
color,
textAlign,
buttons,
content,
children,
verticalAlign = 'baseline',
} = props;
return (
<tr className={classes(['LabeledList__row', className])}>
<Box
as="td"
color={labelColor}
className={classes([
'LabeledList__cell',
// Kinda flipped because we want nowrap as default. Cleaner CSS this way though.
!labelWrap && 'LabeledList__label--nowrap',
])}
verticalAlign={verticalAlign}>
{label ? (typeof label === 'string' ? label + ':' : label) : null}
</Box>
<Box
as="td"
color={color}
textAlign={textAlign}
className={classes(['LabeledList__cell', 'LabeledList__content'])}
colSpan={buttons ? undefined : 2}
verticalAlign={verticalAlign}>
{content}
{children}
</Box>
{buttons && (
<td className="LabeledList__cell LabeledList__buttons">{buttons}</td>
)}
</tr>
);
};
LabeledListItem.defaultHooks = pureComponentHooks;
type LabeledListDividerProps = {
size?: number;
};
const LabeledListDivider = (props: LabeledListDividerProps) => {
const padding = props.size ? unit(Math.max(0, props.size - 1)) : 0;
return (
<tr className="LabeledList__row">
<td
colSpan={3}
style={{
'padding-top': padding,
'padding-bottom': padding,
}}>
<Divider />
</td>
</tr>
);
};
LabeledListDivider.defaultHooks = pureComponentHooks;
LabeledList.Item = LabeledListItem;
LabeledList.Divider = LabeledListDivider;

View File

@@ -1,231 +0,0 @@
/**
* @file
* @copyright 2022 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { Component, createRef, InfernoNode, RefObject } from 'inferno';
import { Box } from './Box';
import { logger } from '../logging';
import { Icon } from './Icon';
type MenuProps = {
children: any;
width: string;
menuRef: RefObject<HTMLElement>;
onOutsideClick: () => void;
};
class Menu extends Component<MenuProps> {
private readonly handleClick: (event) => void;
constructor(props) {
super(props);
this.handleClick = (event) => {
if (!this.props.menuRef.current) {
logger.log(`Menu.handleClick(): No ref`);
return;
}
if (this.props.menuRef.current.contains(event.target)) {
logger.log(`Menu.handleClick(): Inside`);
} else {
logger.log(`Menu.handleClick(): Outside`);
this.props.onOutsideClick();
}
};
}
// eslint-disable-next-line react/no-deprecated
componentWillMount() {
window.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
window.removeEventListener('click', this.handleClick);
}
render() {
const { width, children } = this.props;
return (
<div
className={'MenuBar__menu'}
style={{
width: width,
}}>
{children}
</div>
);
}
}
type MenuBarDropdownProps = {
open: boolean;
openWidth: string;
children: any;
disabled?: boolean;
display: any;
onMouseOver: () => void;
onClick: () => void;
onOutsideClick: () => void;
className?: string;
};
class MenuBarButton extends Component<MenuBarDropdownProps> {
private readonly menuRef: RefObject<HTMLDivElement>;
constructor(props) {
super(props);
this.menuRef = createRef();
}
render() {
const { props } = this;
const {
open,
openWidth,
children,
disabled,
display,
onMouseOver,
onClick,
onOutsideClick,
...boxProps
} = props;
const { className, ...rest } = boxProps;
return (
<div ref={this.menuRef}>
<Box
className={classes([
'MenuBar__MenuBarButton',
'MenuBar__font',
'MenuBar__hover',
className,
])}
{...rest}
onClick={disabled ? undefined : onClick}
onmouseover={onMouseOver}>
<span className="MenuBar__MenuBarButton-text">{display}</span>
</Box>
{open && (
<Menu
width={openWidth}
menuRef={this.menuRef}
onOutsideClick={onOutsideClick}>
{children}
</Menu>
)}
</div>
);
}
}
type MenuBarItemProps = {
entry: string;
children: any;
openWidth: string;
display: InfernoNode;
setOpenMenuBar: (entry: string | null) => void;
openMenuBar: string | null;
setOpenOnHover: (flag: boolean) => void;
openOnHover: boolean;
disabled?: boolean;
className?: string;
};
export const Dropdown = (props: MenuBarItemProps) => {
const {
entry,
children,
openWidth,
display,
setOpenMenuBar,
openMenuBar,
setOpenOnHover,
openOnHover,
disabled,
className,
} = props;
return (
<MenuBarButton
openWidth={openWidth}
display={display}
disabled={disabled}
open={openMenuBar === entry}
className={className}
onClick={() => {
const open = openMenuBar === entry ? null : entry;
setOpenMenuBar(open);
setOpenOnHover(!openOnHover);
}}
onOutsideClick={() => {
setOpenMenuBar(null);
setOpenOnHover(false);
}}
onMouseOver={() => {
if (openOnHover) {
setOpenMenuBar(entry);
}
}}>
{children}
</MenuBarButton>
);
};
const MenuItemToggle = (props) => {
const { value, displayText, onClick, checked } = props;
return (
<Box
className={classes([
'MenuBar__font',
'MenuBar__MenuItem',
'MenuBar__MenuItemToggle',
'MenuBar__hover',
])}
onClick={() => onClick(value)}>
<div className="MenuBar__MenuItemToggle__check">
{checked && <Icon size={1.3} name="check" />}
</div>
{displayText}
</Box>
);
};
Dropdown.MenuItemToggle = MenuItemToggle;
const MenuItem = (props) => {
const { value, displayText, onClick } = props;
return (
<Box
className={classes([
'MenuBar__font',
'MenuBar__MenuItem',
'MenuBar__hover',
])}
onClick={() => onClick(value)}>
{displayText}
</Box>
);
};
Dropdown.MenuItem = MenuItem;
const Separator = () => {
return <div className="MenuBar__Separator" />;
};
Dropdown.Separator = Separator;
type MenuBarProps = {
children: any;
};
export const MenuBar = (props: MenuBarProps) => {
const { children } = props;
return <Box className="MenuBar">{children}</Box>;
};
MenuBar.Dropdown = Dropdown;

View File

@@ -1,33 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
import { Dimmer } from './Dimmer';
export const Modal = (props) => {
const { className, children, onEnter, ...rest } = props;
// VOREStation Addition start
let handleKeyDown;
if (onEnter) {
handleKeyDown = (e) => {
let key = e.which || e.keyCode;
if (key === 13) {
onEnter(e);
}
};
}
// VOREStation Addition end
return (
<Dimmer onKeyDown={handleKeyDown} /* VOREStation edit */>
<div
className={classes(['Modal', className, computeBoxClassName(rest)])}
{...computeBoxProps(rest)}>
{children}
</div>
</Dimmer>
);
};

View File

@@ -1,223 +0,0 @@
import { Component } from 'inferno';
import { Box, Button, Icon, Tooltip, LabeledList, Slider } from '.';
import { useBackend } from '../backend';
const pauseEvent = (e) => {
if (e.stopPropagation) {
e.stopPropagation();
}
if (e.preventDefault) {
e.preventDefault();
}
e.cancelBubble = true;
e.returnValue = false;
return false;
};
const zoomScale = 280;
export class NanoMap extends Component {
constructor(props) {
super(props);
// Auto center based on window size
const Xcenter = window.innerWidth / 2 - 256;
const Ycenter = window.innerHeight / 2 - 256;
this.state = {
offsetX: Xcenter,
offsetY: Ycenter,
transform: 'none',
dragging: false,
originX: null,
originY: null,
zoom: 1,
};
// Dragging
this.handleDragStart = (e) => {
this.ref = e.target;
this.setState({
dragging: false,
originX: e.screenX,
originY: e.screenY,
});
document.addEventListener('mousemove', this.handleDragMove);
document.addEventListener('mouseup', this.handleDragEnd);
pauseEvent(e);
};
this.handleDragMove = (e) => {
this.setState((prevState) => {
const state = { ...prevState };
const newOffsetX = e.screenX - state.originX;
const newOffsetY = e.screenY - state.originY;
if (prevState.dragging) {
state.offsetX += newOffsetX;
state.offsetY += newOffsetY;
state.originX = e.screenX;
state.originY = e.screenY;
} else {
state.dragging = true;
}
return state;
});
pauseEvent(e);
};
this.handleDragEnd = (e) => {
this.setState({
dragging: false,
originX: null,
originY: null,
});
document.removeEventListener('mousemove', this.handleDragMove);
document.removeEventListener('mouseup', this.handleDragEnd);
pauseEvent(e);
};
this.handleOnClick = (e) => {
let byondX = e.offsetX / this.state.zoom / zoomScale;
let byondY = 1 - e.offsetY / this.state.zoom / zoomScale; // Byond origin is bottom left, this is top left
e.byondX = byondX;
e.byondY = byondY;
if (typeof this.props.onClick === 'function') {
this.props.onClick(e);
}
};
this.handleZoom = (_e, value) => {
this.setState((state) => {
const newZoom = Math.min(Math.max(value, 1), 8);
let zoomDiff = (newZoom - state.zoom) * 1.5;
state.zoom = newZoom;
let newOffsetX = state.offsetX - 262 * zoomDiff;
if (newOffsetX < -500) {
newOffsetX = -500;
}
if (newOffsetX > 500) {
newOffsetX = 500;
}
let newOffsetY = state.offsetY - 256 * zoomDiff;
if (newOffsetY < -200) {
newOffsetY = -200;
}
if (newOffsetY > 200) {
newOffsetY = 200;
}
state.offsetX = newOffsetX;
state.offsetY = newOffsetY;
if (props.onZoom) {
props.onZoom(state.zoom);
}
return state;
});
};
}
render() {
const { config } = useBackend(this.context);
const { dragging, offsetX, offsetY, zoom = 1 } = this.state;
const { children } = this.props;
const mapUrl = config.map + '_nanomap_z' + config.mapZLevel + '.png';
// (x * zoom), x Needs to be double the turf- map size. (for virgo, 140x140)
const mapSize = zoomScale * zoom + 'px';
const newStyle = {
width: mapSize,
height: mapSize,
'margin-top': offsetY + 'px',
'margin-left': offsetX + 'px',
'overflow': 'hidden',
'position': 'relative',
'background-image': 'url(' + mapUrl + ')',
'background-size': 'cover',
'background-repeat': 'no-repeat',
'text-align': 'center',
'cursor': dragging ? 'move' : 'auto',
};
return (
<Box className="NanoMap__container">
<Box
style={newStyle}
textAlign="center"
onMouseDown={this.handleDragStart}
onClick={this.handleOnClick}>
<Box>{children}</Box>
</Box>
<NanoMapZoomer zoom={zoom} onZoom={this.handleZoom} />
</Box>
);
}
}
const NanoMapMarker = (props, context) => {
const { x, y, zoom = 1, icon, tooltip, color, onClick } = props;
const handleOnClick = (e) => {
pauseEvent(e);
if (onClick) {
onClick(e);
}
};
const rx = x * 2 * zoom - zoom - 3;
const ry = y * 2 * zoom - zoom - 3;
return (
<div>
<Box
position="absolute"
className="NanoMap__marker"
lineHeight="0"
bottom={ry + 'px'}
left={rx + 'px'}
onMouseDown={handleOnClick}>
<Icon name={icon} color={color} fontSize="6px" />
<Tooltip content={tooltip} />
</Box>
</div>
);
};
NanoMap.Marker = NanoMapMarker;
const NanoMapZoomer = (props, context) => {
const { act, config, data } = useBackend(context);
return (
<Box className="NanoMap__zoomer">
<LabeledList>
<LabeledList.Item label="Zoom">
<Slider
minValue="1"
maxValue="8"
stepPixelSize="10"
format={(v) => v + 'x'}
value={props.zoom}
onDrag={(e, v) => props.onZoom(e, v)}
/>
</LabeledList.Item>
<LabeledList.Item label="Z-Level">
{data.map_levels
.sort((a, b) => Number(a) - Number(b))
.map((level) => (
<Button
key={level}
selected={~~level === ~~config.mapZLevel}
content={level}
onClick={() => {
act('setZLevel', { 'mapZLevel': level });
}}
/>
))}
</LabeledList.Item>
</LabeledList>
</Box>
);
};
NanoMap.Zoomer = NanoMapZoomer;

View File

@@ -1,27 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { Box } from './Box';
export const NoticeBox = (props) => {
const { className, color, info, warning, success, danger, ...rest } = props;
return (
<Box
className={classes([
'NoticeBox',
color && 'NoticeBox--color--' + color,
info && 'NoticeBox--type--info',
success && 'NoticeBox--type--success',
danger && 'NoticeBox--type--danger',
className,
])}
{...rest}
/>
);
};
NoticeBox.defaultHooks = pureComponentHooks;

View File

@@ -1,284 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { clamp } from 'common/math';
import { classes, pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
import { AnimatedNumber } from './AnimatedNumber';
import { Box } from './Box';
const DEFAULT_UPDATE_RATE = 400;
export class NumberInput extends Component {
constructor(props) {
super(props);
const { value } = props;
this.inputRef = createRef();
this.state = {
value,
dragging: false,
editing: false,
internalValue: null,
origin: null,
suppressingFlicker: false,
};
// Suppresses flickering while the value propagates through the backend
this.flickerTimer = null;
this.suppressFlicker = () => {
const { suppressFlicker } = this.props;
if (suppressFlicker > 0) {
this.setState({
suppressingFlicker: true,
});
clearTimeout(this.flickerTimer);
this.flickerTimer = setTimeout(
() =>
this.setState({
suppressingFlicker: false,
}),
suppressFlicker
);
}
};
this.handleDragStart = (e) => {
const { value } = this.props;
const { editing } = this.state;
if (editing) {
return;
}
document.body.style['pointer-events'] = 'none';
this.ref = e.target;
this.setState({
dragging: false,
origin: e.screenY,
value,
internalValue: value,
});
this.timer = setTimeout(() => {
this.setState({
dragging: true,
});
}, 250);
this.dragInterval = setInterval(() => {
const { dragging, value } = this.state;
const { onDrag } = this.props;
if (dragging && onDrag) {
onDrag(e, value);
}
}, this.props.updateRate || DEFAULT_UPDATE_RATE);
document.addEventListener('mousemove', this.handleDragMove);
document.addEventListener('mouseup', this.handleDragEnd);
};
this.handleDragMove = (e) => {
const { minValue, maxValue, step, stepPixelSize } = this.props;
this.setState((prevState) => {
const state = { ...prevState };
const offset = state.origin - e.screenY;
if (prevState.dragging) {
const stepOffset = Number.isFinite(minValue) ? minValue % step : 0;
// Translate mouse movement to value
// Give it some headroom (by increasing clamp range by 1 step)
state.internalValue = clamp(
state.internalValue + (offset * step) / stepPixelSize,
minValue - step,
maxValue + step
);
// Clamp the final value
state.value = clamp(
state.internalValue - (state.internalValue % step) + stepOffset,
minValue,
maxValue
);
state.origin = e.screenY;
} else if (Math.abs(offset) > 4) {
state.dragging = true;
}
return state;
});
};
this.handleDragEnd = (e) => {
const { onChange, onDrag } = this.props;
const { dragging, value, internalValue } = this.state;
document.body.style['pointer-events'] = 'auto';
clearTimeout(this.timer);
clearInterval(this.dragInterval);
this.setState({
dragging: false,
editing: !dragging,
origin: null,
});
document.removeEventListener('mousemove', this.handleDragMove);
document.removeEventListener('mouseup', this.handleDragEnd);
if (dragging) {
this.suppressFlicker();
if (onChange) {
onChange(e, value);
}
if (onDrag) {
onDrag(e, value);
}
} else if (this.inputRef) {
const input = this.inputRef.current;
input.value = internalValue;
// IE8: Dies when trying to focus a hidden element
// (Error: Object does not support this action)
try {
input.focus();
input.select();
} catch {}
}
};
}
render() {
const {
dragging,
editing,
value: intermediateValue,
suppressingFlicker,
} = this.state;
const {
className,
fluid,
animated,
value,
unit,
minValue,
maxValue,
height,
width,
lineHeight,
fontSize,
format,
onChange,
onDrag,
} = this.props;
let displayValue = value;
if (dragging || suppressingFlicker) {
displayValue = intermediateValue;
}
// prettier-ignore
const contentElement = (
<div className="NumberInput__content" unselectable={Byond.IS_LTE_IE8}>
{
(animated && !dragging && !suppressingFlicker) ?
(<AnimatedNumber value={displayValue} format={format} />) :
(format ? format(displayValue) : displayValue)
}
{unit ? ' ' + unit : ''}
</div>
);
return (
<Box
className={classes([
'NumberInput',
fluid && 'NumberInput--fluid',
className,
])}
minWidth={width}
minHeight={height}
lineHeight={lineHeight}
fontSize={fontSize}
onMouseDown={this.handleDragStart}>
<div className="NumberInput__barContainer">
<div
className="NumberInput__bar"
style={{
// prettier-ignore
height: clamp(
(displayValue - minValue) / (maxValue - minValue) * 100,
0, 100) + '%',
}}
/>
</div>
{contentElement}
<input
ref={this.inputRef}
className="NumberInput__input"
style={{
display: !editing ? 'none' : undefined,
height: height,
'line-height': lineHeight,
'font-size': fontSize,
}}
onBlur={(e) => {
if (!editing) {
return;
}
const value = clamp(parseFloat(e.target.value), minValue, maxValue);
if (Number.isNaN(value)) {
this.setState({
editing: false,
});
return;
}
this.setState({
editing: false,
value,
});
this.suppressFlicker();
if (onChange) {
onChange(e, value);
}
if (onDrag) {
onDrag(e, value);
}
}}
onKeyDown={(e) => {
if (e.keyCode === 13) {
// prettier-ignore
const value = clamp(
parseFloat(e.target.value),
minValue,
maxValue
);
if (Number.isNaN(value)) {
this.setState({
editing: false,
});
return;
}
this.setState({
editing: false,
value,
});
this.suppressFlicker();
if (onChange) {
onChange(e, value);
}
if (onDrag) {
onDrag(e, value);
}
return;
}
if (e.keyCode === 27) {
this.setState({
editing: false,
});
return;
}
}}
/>
</Box>
);
}
}
NumberInput.defaultHooks = pureComponentHooks;
NumberInput.defaultProps = {
minValue: -Infinity,
maxValue: +Infinity,
step: 1,
stepPixelSize: 1,
suppressFlicker: 50,
};

View File

@@ -1,84 +0,0 @@
import { createPopper } from '@popperjs/core';
import { ArgumentsOf } from 'common/types';
import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';
type PopperProps = {
popperContent: InfernoNode;
options?: ArgumentsOf<typeof createPopper>[2];
additionalStyles?: CSSProperties;
};
export class Popper extends Component<PopperProps> {
static id: number = 0;
renderedContent: HTMLDivElement;
popperInstance: ReturnType<typeof createPopper>;
constructor() {
super();
Popper.id += 1;
}
componentDidMount() {
const { additionalStyles, options } = this.props;
this.renderedContent = document.createElement('div');
if (additionalStyles) {
for (const [attribute, value] of Object.entries(additionalStyles)) {
this.renderedContent.style[attribute] = value;
}
}
this.renderPopperContent(() => {
document.body.appendChild(this.renderedContent);
// HACK: We don't want to create a wrapper, as it could break the layout
// of consumers, so we do the inferno equivalent of `findDOMNode(this)`.
// This is usually bad as refs are usually better, but refs did
// not work in this case, as they weren't propagating correctly.
// A previous attempt was made as a render prop that passed an ID,
// but this made consuming use too unwieldly.
// This code is copied from `findDOMNode` in inferno-extras.
// Because this component is written in TypeScript, we will know
// immediately if this internal variable is removed.
const domNode = findDOMfromVNode(this.$LI, true);
if (!domNode) {
return;
}
this.popperInstance = createPopper(
domNode,
this.renderedContent,
options
);
});
}
componentDidUpdate() {
this.renderPopperContent(() => this.popperInstance?.update());
}
componentWillUnmount() {
this.popperInstance?.destroy();
render(null, this.renderedContent, () => {
this.renderedContent.remove();
});
}
renderPopperContent(callback: () => void) {
// `render` errors when given false, so we convert it to `null`,
// which is supported.
render(
this.props.popperContent || null,
this.renderedContent,
callback,
this.context
);
}
render() {
return this.props.children;
}
}

View File

@@ -1,66 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { clamp01, scale, keyOfMatchingRange, toFixed } from 'common/math';
import { classes, pureComponentHooks } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
import { CSS_COLORS } from '../constants';
export const ProgressBar = (props) => {
const {
className,
value,
minValue = 0,
maxValue = 1,
color,
ranges = {},
children,
...rest
} = props;
const scaledValue = scale(value, minValue, maxValue);
const hasContent = children !== undefined;
// prettier-ignore
const effectiveColor = color
|| keyOfMatchingRange(value, ranges)
|| 'default';
// We permit colors to be in hex format, rgb()/rgba() format,
// a name for a color-<name> class, or a base CSS class.
const outerProps = computeBoxProps(rest);
// prettier-ignore
const outerClasses = [
'ProgressBar',
className,
computeBoxClassName(rest),
];
const fillStyles = {
'width': clamp01(scaledValue) * 100 + '%',
};
if (CSS_COLORS.includes(effectiveColor) || effectiveColor === 'default') {
// If the color is a color-<name> class, just use that.
outerClasses.push('ProgressBar--color--' + effectiveColor);
} else {
// Otherwise, set styles directly.
// prettier-ignore
outerProps.style = (outerProps.style || "")
+ `border-color: ${effectiveColor};`;
fillStyles['background-color'] = effectiveColor;
}
return (
<div className={classes(outerClasses)} {...outerProps}>
<div
className="ProgressBar__fill ProgressBar__fill--animated"
style={fillStyles}
/>
<div className="ProgressBar__content">
{hasContent ? children : toFixed(scaledValue * 100) + '%'}
</div>
</div>
);
};
ProgressBar.defaultHooks = pureComponentHooks;

View File

@@ -1,176 +0,0 @@
import { classes } from 'common/react';
import { clamp } from 'common/math';
import { Component, createRef } from 'inferno';
import { Box } from './Box';
import { KEY_ESCAPE, KEY_ENTER } from 'common/keycodes';
const DEFAULT_MIN = 0;
const DEFAULT_MAX = 10000;
/**
* Takes a string input and parses integers or floats from it.
* If none: Minimum is set.
* Else: Clamps it to the given range.
*/
const getClampedNumber = (value, minValue, maxValue, allowFloats) => {
const minimum = minValue || DEFAULT_MIN;
const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
if (!value || !value.length) {
return String(minimum);
}
let parsedValue = allowFloats
? parseFloat(value.replace(/[^\-\d.]/g, ''))
: parseInt(value.replace(/[^\-\d]/g, ''), 10);
if (isNaN(parsedValue)) {
return String(minimum);
} else {
return String(clamp(parsedValue, minimum, maximum));
}
};
export class RestrictedInput extends Component {
constructor() {
super();
this.inputRef = createRef();
this.state = {
editing: false,
};
this.handleBlur = (e) => {
const { editing } = this.state;
if (editing) {
this.setEditing(false);
}
};
this.handleChange = (e) => {
const { maxValue, minValue, onChange, allowFloats } = this.props;
e.target.value = getClampedNumber(
e.target.value,
minValue,
maxValue,
allowFloats
);
if (onChange) {
onChange(e, +e.target.value);
}
};
this.handleFocus = (e) => {
const { editing } = this.state;
if (!editing) {
this.setEditing(true);
}
};
this.handleInput = (e) => {
const { editing } = this.state;
const { onInput } = this.props;
if (!editing) {
this.setEditing(true);
}
if (onInput) {
onInput(e, +e.target.value);
}
};
this.handleKeyDown = (e) => {
const { maxValue, minValue, onChange, onEnter, allowFloats } = this.props;
if (e.keyCode === KEY_ENTER) {
const safeNum = getClampedNumber(
e.target.value,
minValue,
maxValue,
allowFloats
);
this.setEditing(false);
if (onChange) {
onChange(e, +safeNum);
}
if (onEnter) {
onEnter(e, +safeNum);
}
e.target.blur();
return;
}
if (e.keyCode === KEY_ESCAPE) {
if (this.props.onEscape) {
this.props.onEscape(e);
return;
}
this.setEditing(false);
e.target.value = this.props.value;
e.target.blur();
return;
}
};
}
componentDidMount() {
const { maxValue, minValue, allowFloats } = this.props;
const nextValue = this.props.value?.toString();
const input = this.inputRef.current;
if (input) {
input.value = getClampedNumber(
nextValue,
minValue,
maxValue,
allowFloats
);
}
if (this.props.autoFocus || this.props.autoSelect) {
setTimeout(() => {
input.focus();
if (this.props.autoSelect) {
input.select();
}
}, 1);
}
}
componentDidUpdate(prevProps, _) {
const { maxValue, minValue, allowFloats } = this.props;
const { editing } = this.state;
const prevValue = prevProps.value?.toString();
const nextValue = this.props.value?.toString();
const input = this.inputRef.current;
if (input && !editing) {
if (nextValue !== prevValue && nextValue !== input.value) {
input.value = getClampedNumber(
nextValue,
minValue,
maxValue,
allowFloats
);
}
}
}
setEditing(editing) {
this.setState({ editing });
}
render() {
const { props } = this;
const { onChange, onEnter, onInput, value, ...boxProps } = props;
const { className, fluid, monospace, ...rest } = boxProps;
return (
<Box
className={classes([
'Input',
fluid && 'Input--fluid',
monospace && 'Input--monospace',
className,
])}
{...rest}>
<div className="Input__baseline">.</div>
<input
className="Input__input"
onChange={this.handleChange}
onInput={this.handleInput}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
ref={this.inputRef}
type="number"
/>
</Box>
);
}
}

View File

@@ -1,134 +0,0 @@
/**
* @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,
alertBefore,
format,
size = 1,
className,
style,
...rest
} = props;
const scaledValue = scale(value, minValue, maxValue);
const clampedValue = clamp01(scaledValue);
const 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),
];
});
}
const shouldShowAlert = () => {
// If both after and before alert props are set, attempt to interpret both
// in a helpful way.
if (alertAfter && alertBefore && alertAfter < alertBefore) {
// If alertAfter is before alertBefore, only display an alert if
// we're between them.
if (alertAfter < value && alertBefore > value) {
return true;
}
} else if (alertAfter < value || alertBefore > value) {
// Otherwise, we have distint ranges, or only one or neither are set.
// Either way, being on the active side of either is sufficient.
return true;
}
return false;
};
// prettier-ignore
const alertColor = shouldShowAlert()
&& 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 || alertBefore) && (
<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>
);
};

View File

@@ -1,116 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { canRender, classes } from 'common/react';
import { Component, createRef, InfernoNode, RefObject } from 'inferno';
import { addScrollableNode, removeScrollableNode } from '../events';
import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
interface SectionProps extends BoxProps {
className?: string;
title?: string | InfernoElement<string>;
buttons?: InfernoNode;
fill?: boolean;
fitted?: boolean;
scrollable?: boolean;
scrollableHorizontal?: boolean;
flexGrow?: boolean; // VOREStation Addition
noTopPadding?: boolean; // VOREStation Addition
stretchContents?: boolean; // VOREStation Addition
/** @deprecated This property no longer works, please remove it. */
level?: never;
/** @deprecated Please use `scrollable` property */
overflowY?: never;
/** @member Allows external control of scrolling. */
scrollableRef?: RefObject<HTMLDivElement>;
/** @member Callback function for the `scroll` event */
onScroll?: (this: GlobalEventHandlers, ev: Event) => any;
}
export class Section extends Component<SectionProps> {
scrollableRef: RefObject<HTMLDivElement>;
scrollable: boolean;
onScroll?: (this: GlobalEventHandlers, ev: Event) => any;
scrollableHorizontal: boolean;
constructor(props) {
super(props);
this.scrollableRef = props.scrollableRef || createRef();
this.scrollable = props.scrollable;
this.onScroll = props.onScroll;
this.scrollableHorizontal = props.scrollableHorizontal;
}
componentDidMount() {
if (this.scrollable || this.scrollableHorizontal) {
addScrollableNode(this.scrollableRef.current as HTMLElement);
if (this.onScroll && this.scrollableRef.current) {
this.scrollableRef.current.onscroll = this.onScroll;
}
}
}
componentWillUnmount() {
if (this.scrollable || this.scrollableHorizontal) {
removeScrollableNode(this.scrollableRef.current as HTMLElement);
}
}
render() {
const {
className,
title,
buttons,
fill,
fitted,
scrollable,
scrollableHorizontal,
flexGrow, // VOREStation Addition
noTopPadding, // VOREStation Addition
stretchContents, // VOREStation Addition
children,
onScroll,
...rest
} = this.props;
const hasTitle = canRender(title) || canRender(buttons);
return (
<div
className={classes([
'Section',
Byond.IS_LTE_IE8 && 'Section--iefix',
fill && 'Section--fill',
fitted && 'Section--fitted',
scrollable && 'Section--scrollable',
scrollableHorizontal && 'Section--scrollableHorizontal',
flexGrow && 'Section--flex', // VOREStation Addition
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
{hasTitle && (
<div className="Section__title">
<span className="Section__titleText">{title}</span>
<div className="Section__buttons">{buttons}</div>
</div>
)}
<div className="Section__rest">
{/* Vorestation Edit Start */}
<div
ref={this.scrollableRef}
onScroll={onScroll}
className={classes([
'Section__content',
!!stretchContents && 'Section__content--stretchContents',
!!noTopPadding && 'Section__content--noTopPadding',
])}>
{children}
</div>
{/* Vorestation Edit End */}
</div>
</div>
);
}
}

View File

@@ -1,126 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
import { DraggableControl } from './DraggableControl';
import { NumberInput } from './NumberInput';
export const Slider = (props) => {
// IE8: I don't want to support a yet another component on IE8.
if (Byond.IS_LTE_IE8) {
return <NumberInput {...props} />;
}
const {
// Draggable props (passthrough)
animated,
format,
maxValue,
minValue,
onChange,
onDrag,
step,
stepPixelSize,
suppressFlicker,
unit,
value,
// Own props
className,
fillValue,
color,
ranges = {},
children,
...rest
} = props;
const hasContent = children !== undefined;
return (
<DraggableControl
dragMatrix={[1, 0]}
{...{
animated,
format,
maxValue,
minValue,
onChange,
onDrag,
step,
stepPixelSize,
suppressFlicker,
unit,
value,
}}>
{(control) => {
const {
dragging,
editing,
value,
displayValue,
displayElement,
inputElement,
handleDragStart,
} = control;
const hasFillValue = fillValue !== undefined && fillValue !== null;
const scaledValue = scale(value, minValue, maxValue);
const scaledFillValue = scale(
fillValue ?? displayValue,
minValue,
maxValue
);
const scaledDisplayValue = scale(displayValue, minValue, maxValue);
// prettier-ignore
const effectiveColor = color
|| keyOfMatchingRange(fillValue ?? value, ranges) || 'default';
return (
<div
className={classes([
'Slider',
'ProgressBar',
'ProgressBar--color--' + effectiveColor,
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}
onMouseDown={handleDragStart}>
<div
className={classes([
'ProgressBar__fill',
hasFillValue && 'ProgressBar__fill--animated',
])}
style={{
width: clamp01(scaledFillValue) * 100 + '%',
opacity: 0.4,
}}
/>
<div
className="ProgressBar__fill"
style={{
// prettier-ignore
width: clamp01(Math.min(scaledFillValue, scaledDisplayValue))
* 100 + '%',
}}
/>
<div
className="Slider__cursorOffset"
style={{
width: clamp01(scaledDisplayValue) * 100 + '%',
}}>
<div className="Slider__cursor" />
<div className="Slider__pointer" />
{dragging && (
<div className="Slider__popupValue">{displayElement}</div>
)}
</div>
<div className="ProgressBar__content">
{hasContent ? children : displayElement}
</div>
{inputElement}
</div>
);
}}
</DraggableControl>
);
};

View File

@@ -1,76 +0,0 @@
/**
* @file
* @copyright 2021 Aleksej Komarov
* @license MIT
*/
import { classes } from 'common/react';
import { RefObject } from 'inferno';
import { computeFlexClassName, computeFlexItemClassName, computeFlexItemProps, computeFlexProps, FlexItemProps, FlexProps } from './Flex';
type StackProps = FlexProps & {
vertical?: boolean;
fill?: boolean;
};
export const Stack = (props: StackProps) => {
const { className, vertical, fill, ...rest } = props;
return (
<div
className={classes([
'Stack',
fill && 'Stack--fill',
vertical ? 'Stack--vertical' : 'Stack--horizontal',
className,
computeFlexClassName(props),
])}
{...computeFlexProps({
direction: vertical ? 'column' : 'row',
...rest,
})}
/>
);
};
type StackItemProps = FlexProps & {
innerRef?: RefObject<HTMLDivElement>;
};
const StackItem = (props: StackItemProps) => {
const { className, innerRef, ...rest } = props;
return (
<div
className={classes([
'Stack__item',
className,
computeFlexItemClassName(rest),
])}
ref={innerRef}
{...computeFlexItemProps(rest)}
/>
);
};
Stack.Item = StackItem;
type StackDividerProps = FlexItemProps & {
hidden?: boolean;
};
const StackDivider = (props: StackDividerProps) => {
const { className, hidden, ...rest } = props;
return (
<div
className={classes([
'Stack__item',
'Stack__divider',
hidden && 'Stack__divider--hidden',
className,
computeFlexItemClassName(rest),
])}
{...computeFlexItemProps(rest)}
/>
);
};
Stack.Divider = StackDivider;

View File

@@ -1,26 +0,0 @@
import { SFC } from 'inferno';
import { Box } from './Box';
// The cost of flexibility and prettiness.
export const StyleableSection: SFC<{
style?;
titleStyle?;
textStyle?;
title?;
titleSubtext?;
}> = (props) => {
return (
<Box style={props.style}>
{/* Yes, this box (line above) is missing the "Section" class. This is very intentional, as the layout looks *ugly* with it.*/}
<Box class="Section__title" style={props.titleStyle}>
<Box class="Section__titleText" style={props.textStyle}>
{props.title}
</Box>
<div className="Section__buttons">{props.titleSubtext}</div>
</Box>
<Box class="Section__rest">
<Box class="Section__content">{props.children}</Box>
</Box>
</Box>
);
};

View File

@@ -1,64 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { classes, pureComponentHooks } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
export const Table = (props) => {
const { className, collapsing, children, ...rest } = props;
return (
<table
className={classes([
'Table',
collapsing && 'Table--collapsing',
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
<tbody>{children}</tbody>
</table>
);
};
Table.defaultHooks = pureComponentHooks;
export const TableRow = (props) => {
const { className, header, ...rest } = props;
return (
<tr
className={classes([
'Table__row',
header && 'Table__row--header',
className,
computeBoxClassName(props),
])}
{...computeBoxProps(rest)}
/>
);
};
TableRow.defaultHooks = pureComponentHooks;
export const TableCell = (props) => {
const { className, collapsing, header, ...rest } = props;
return (
<td
className={classes([
'Table__cell',
collapsing && 'Table__cell--collapsing',
header && 'Table__cell--header',
className,
computeBoxClassName(props),
])}
{...computeBoxProps(rest)}
/>
);
};
TableCell.defaultHooks = pureComponentHooks;
Table.Row = TableRow;
Table.Cell = TableCell;

View File

@@ -1,63 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { canRender, classes } from 'common/react';
import { computeBoxClassName, computeBoxProps } from './Box';
import { Icon } from './Icon';
export const Tabs = (props) => {
const { className, vertical, fill, fluid, children, ...rest } = props;
return (
<div
className={classes([
'Tabs',
vertical ? 'Tabs--vertical' : 'Tabs--horizontal',
fill && 'Tabs--fill',
fluid && 'Tabs--fluid',
className,
computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
{children}
</div>
);
};
const Tab = (props) => {
const {
className,
selected,
color,
icon,
leftSlot,
rightSlot,
children,
...rest
} = props;
return (
<div
className={classes([
'Tab',
'Tabs__Tab',
'Tab--color--' + color,
selected && 'Tab--selected',
className,
...computeBoxClassName(rest),
])}
{...computeBoxProps(rest)}>
{(canRender(leftSlot) && <div className="Tab__left">{leftSlot}</div>) ||
(!!icon && (
<div className="Tab__left">
<Icon name={icon} />
</div>
))}
<div className="Tab__text">{children}</div>
{canRender(rightSlot) && <div className="Tab__right">{rightSlot}</div>}
</div>
);
};
Tabs.Tab = Tab;

View File

@@ -1,238 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @author Warlockd
* @license MIT
*/
import { classes } from 'common/react';
import { Component, createRef } from 'inferno';
import { Box } from './Box';
import { toInputValue } from './Input';
import { KEY_ENTER, KEY_ESCAPE, KEY_TAB } from 'common/keycodes';
export class TextArea extends Component {
constructor(props, context) {
super(props, context);
this.textareaRef = props.innerRef || createRef();
this.state = {
editing: false,
scrolledAmount: 0,
};
const { dontUseTabForIndent = false } = props;
this.handleOnInput = (e) => {
const { editing } = this.state;
const { onInput } = this.props;
if (!editing) {
this.setEditing(true);
}
if (onInput) {
onInput(e, e.target.value);
}
};
this.handleOnChange = (e) => {
const { editing } = this.state;
const { onChange } = this.props;
if (editing) {
this.setEditing(false);
}
if (onChange) {
onChange(e, e.target.value);
}
};
this.handleKeyPress = (e) => {
const { editing } = this.state;
const { onKeyPress } = this.props;
if (!editing) {
this.setEditing(true);
}
if (onKeyPress) {
onKeyPress(e, e.target.value);
}
};
this.handleKeyDown = (e) => {
const { editing } = this.state;
const { onChange, onInput, onEnter, onKey } = this.props;
if (e.keyCode === KEY_ENTER) {
this.setEditing(false);
if (onChange) {
onChange(e, e.target.value);
}
if (onInput) {
onInput(e, e.target.value);
}
if (onEnter) {
onEnter(e, e.target.value);
}
if (this.props.selfClear) {
e.target.value = '';
e.target.blur();
}
return;
}
if (e.keyCode === KEY_ESCAPE) {
if (this.props.onEscape) {
this.props.onEscape(e);
}
this.setEditing(false);
if (this.props.selfClear) {
e.target.value = '';
} else {
e.target.value = toInputValue(this.props.value);
e.target.blur();
}
return;
}
if (!editing) {
this.setEditing(true);
}
// Custom key handler
if (onKey) {
onKey(e, e.target.value);
}
if (!dontUseTabForIndent) {
const keyCode = e.keyCode || e.which;
if (keyCode === KEY_TAB) {
e.preventDefault();
const { value, selectionStart, selectionEnd } = e.target;
e.target.value =
value.substring(0, selectionStart) +
'\t' +
value.substring(selectionEnd);
e.target.selectionEnd = selectionStart + 1;
if (onInput) {
onInput(e, e.target.value);
}
}
}
};
this.handleFocus = (e) => {
const { editing } = this.state;
if (!editing) {
this.setEditing(true);
}
};
this.handleBlur = (e) => {
const { editing } = this.state;
const { onChange } = this.props;
if (editing) {
this.setEditing(false);
if (onChange) {
onChange(e, e.target.value);
}
}
};
this.handleScroll = (e) => {
const { displayedValue } = this.props;
const input = this.textareaRef.current;
if (displayedValue && input) {
this.setState({
scrolledAmount: input.scrollTop,
});
}
};
}
componentDidMount() {
const nextValue = this.props.value;
const input = this.textareaRef.current;
if (input) {
input.value = toInputValue(nextValue);
}
if (this.props.autoFocus || this.props.autoSelect) {
setTimeout(() => {
input.focus();
if (this.props.autoSelect) {
input.select();
}
}, 1);
}
}
componentDidUpdate(prevProps, prevState) {
const prevValue = prevProps.value;
const nextValue = this.props.value;
const input = this.textareaRef.current;
if (input && typeof nextValue === 'string' && prevValue !== nextValue) {
input.value = toInputValue(nextValue);
}
}
setEditing(editing) {
this.setState({ editing });
}
getValue() {
return this.textareaRef.current && this.textareaRef.current.value;
}
render() {
// Input only props
const {
onChange,
onKeyDown,
onKeyPress,
onInput,
onFocus,
onBlur,
onEnter,
value,
maxLength,
placeholder,
scrollbar,
noborder,
displayedValue,
...boxProps
} = this.props;
// Box props
const { className, fluid, nowrap, ...rest } = boxProps;
const { scrolledAmount } = this.state;
return (
<Box
className={classes([
'TextArea',
fluid && 'TextArea--fluid',
noborder && 'TextArea--noborder',
className,
])}
{...rest}>
{!!displayedValue && (
<Box position="absolute" width="100%" height="100%" overflow="hidden">
<div
className={classes([
'TextArea__textarea',
'TextArea__textarea_custom',
])}
style={{
'transform': `translateY(-${scrolledAmount}px)`,
}}>
{displayedValue}
</div>
</Box>
)}
<textarea
ref={this.textareaRef}
className={classes([
'TextArea__textarea',
scrollbar && 'TextArea__textarea--scrollable',
nowrap && 'TextArea__nowrap',
])}
placeholder={placeholder}
onChange={this.handleOnChange}
onKeyDown={this.handleKeyDown}
onKeyPress={this.handleKeyPress}
onInput={this.handleOnInput}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onScroll={this.handleScroll}
maxLength={maxLength}
style={{
'color': displayedValue ? 'rgba(0, 0, 0, 0)' : 'inherit',
}}
/>
{/* CHOMPedit End */}
</Box>
);
}
}

View File

@@ -1,63 +0,0 @@
import { formatTime } from '../format';
import { Component } from 'inferno';
// AnimatedNumber Copypaste
const isSafeNumber = (value) => {
return (
typeof value === 'number' && Number.isFinite(value) && !Number.isNaN(value)
);
};
export class TimeDisplay extends Component {
constructor(props) {
super(props);
this.timer = null;
this.last_seen_value = undefined;
this.state = {
value: 0,
};
// Set initial state with value provided in props
if (isSafeNumber(props.value)) {
this.state.value = Number(props.value);
this.last_seen_value = Number(props.value);
}
}
componentDidUpdate() {
if (this.props.auto !== undefined) {
clearInterval(this.timer);
this.timer = setInterval(() => this.tick(), 1000); // every 1 s
}
}
tick() {
let current = Number(this.state.value);
if (this.props.value !== this.last_seen_value) {
this.last_seen_value = this.props.value;
current = this.props.value;
}
const mod = this.props.auto === 'up' ? 10 : -10; // Time down by default.
const value = Math.max(0, current + mod); // one sec tick
this.setState({ value });
}
componentDidMount() {
if (this.props.auto !== undefined) {
this.timer = setInterval(() => this.tick(), 1000); // every 1 s
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
const val = this.state.value;
// Directly display weird stuff
if (!isSafeNumber(val)) {
return this.state.value || null;
}
return formatTime(val);
}
}

View File

@@ -1,150 +0,0 @@
import { createPopper, Placement, VirtualElement } from '@popperjs/core';
import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';
type TooltipProps = {
children?: InfernoNode;
content: InfernoNode;
position?: Placement;
};
type TooltipState = {
hovered: boolean;
};
const DEFAULT_OPTIONS = {
modifiers: [
{
name: 'eventListeners',
enabled: false,
},
],
};
const NULL_RECT: DOMRect = {
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
x: 0,
y: 0,
toJSON: () => null,
};
export class Tooltip extends Component<TooltipProps, TooltipState> {
// Mounting poppers is really laggy because popper.js is very slow.
// Thus, instead of using the Popper component, Tooltip creates ONE popper
// and stores every tooltip inside that.
// This means you can never have two tooltips at once, for instance.
static renderedTooltip: HTMLDivElement | undefined;
static singletonPopper: ReturnType<typeof createPopper> | undefined;
static currentHoveredElement: Element | undefined;
static virtualElement: VirtualElement = {
// prettier-ignore
getBoundingClientRect: () => (
Tooltip.currentHoveredElement?.getBoundingClientRect()
?? NULL_RECT
),
};
getDOMNode() {
// HACK: We don't want to create a wrapper, as it could break the layout
// of consumers, so we do the inferno equivalent of `findDOMNode(this)`.
// My attempt to avoid this was a render prop that passed in
// callbacks to onmouseenter and onmouseleave, but this was unwiedly
// to consumers, specifically buttons.
// This code is copied from `findDOMNode` in inferno-extras.
// Because this component is written in TypeScript, we will know
// immediately if this internal variable is removed.
return findDOMfromVNode(this.$LI, true);
}
componentDidMount() {
const domNode = this.getDOMNode();
if (!domNode) {
return;
}
domNode.addEventListener('mouseenter', () => {
let renderedTooltip = Tooltip.renderedTooltip;
if (renderedTooltip === undefined) {
renderedTooltip = document.createElement('div');
renderedTooltip.className = 'Tooltip';
document.body.appendChild(renderedTooltip);
Tooltip.renderedTooltip = renderedTooltip;
}
Tooltip.currentHoveredElement = domNode;
renderedTooltip.style.opacity = '1';
this.renderPopperContent();
});
domNode.addEventListener('mouseleave', () => {
this.fadeOut();
});
}
fadeOut() {
if (Tooltip.currentHoveredElement !== this.getDOMNode()) {
return;
}
Tooltip.currentHoveredElement = undefined;
Tooltip.renderedTooltip!.style.opacity = '0';
}
renderPopperContent() {
const renderedTooltip = Tooltip.renderedTooltip;
if (!renderedTooltip) {
return;
}
render(
<span>{this.props.content}</span>,
renderedTooltip,
() => {
let singletonPopper = Tooltip.singletonPopper;
if (singletonPopper === undefined) {
singletonPopper = createPopper(
Tooltip.virtualElement,
renderedTooltip!,
{
...DEFAULT_OPTIONS,
placement: this.props.position || 'auto',
}
);
Tooltip.singletonPopper = singletonPopper;
} else {
singletonPopper.setOptions({
...DEFAULT_OPTIONS,
placement: this.props.position || 'auto',
});
singletonPopper.update();
}
},
this.context
);
}
componentDidUpdate() {
if (Tooltip.currentHoveredElement !== this.getDOMNode()) {
return;
}
this.renderPopperContent();
}
componentWillUnmount() {
this.fadeOut();
}
render() {
return this.props.children;
}
}

View File

@@ -1,35 +0,0 @@
import { Component, createRef } from 'inferno';
type Props = {
onOutsideClick: () => void;
};
export class TrackOutsideClicks extends Component<Props> {
ref = createRef<HTMLDivElement>();
constructor() {
super();
this.handleOutsideClick = this.handleOutsideClick.bind(this);
document.addEventListener('click', this.handleOutsideClick);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleOutsideClick);
}
handleOutsideClick(event: MouseEvent) {
if (!(event.target instanceof Node)) {
return;
}
if (this.ref.current && !this.ref.current.contains(event.target)) {
this.props.onOutsideClick();
}
}
render() {
return <div ref={this.ref}>{this.props.children}</div>;
}
}

View File

@@ -1,49 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export { AnimatedNumber } from './AnimatedNumber';
export { Autofocus } from './Autofocus';
export { Blink } from './Blink';
export { BlockQuote } from './BlockQuote';
export { Box } from './Box';
export { Button } from './Button';
export { ByondUi } from './ByondUi';
export { Chart } from './Chart';
export { Collapsible } from './Collapsible';
export { ColorBox } from './ColorBox';
export { Dimmer } from './Dimmer';
export { Divider } from './Divider';
export { DraggableControl } from './DraggableControl';
export { Dropdown } from './Dropdown';
export { Flex } from './Flex';
export { FitText } from './FitText';
export { Grid } from './Grid';
export { Icon } from './Icon';
export { InfinitePlane } from './InfinitePlane';
export { Input } from './Input';
export { KeyListener } from './KeyListener';
export { Knob } from './Knob';
export { LabeledControls } from './LabeledControls';
export { LabeledList } from './LabeledList';
export { MenuBar } from './MenuBar';
export { Modal } from './Modal';
export { NoticeBox } from './NoticeBox';
export { NumberInput } from './NumberInput';
export { ProgressBar } from './ProgressBar';
export { Popper } from './Popper';
export { RestrictedInput } from './RestrictedInput';
export { RoundGauge } from './RoundGauge';
export { Section } from './Section';
export { Slider } from './Slider';
export { StyleableSection } from './StyleableSection';
export { Stack } from './Stack';
export { Table } from './Table';
export { Tabs } from './Tabs';
export { TextArea } from './TextArea';
export { TimeDisplay } from './TimeDisplay';
export { TrackOutsideClicks } from './TrackOutsideClicks';
export { Tooltip } from './Tooltip';
export { Dialog } from './Dialog';

View File

@@ -1,88 +0,0 @@
// import { getGasColor, getGasFromId, getGasFromPath, getGasLabel } from './constants';
import { getGasColor, getGasFromId, getGasLabel } from './constants';
describe('gas helper functions', () => {
it('should get the proper gas label', () => {
// Testing for alphabetic gas id
const gasId = 'oxygen';
const gasLabel = getGasLabel(gasId);
expect(gasLabel).toBe('O₂');
});
it('should get the proper gas label', () => {
// Testing for underscore gas id
const gasId = 'nitrous_oxide';
const gasLabel = getGasLabel(gasId);
expect(gasLabel).toBe('N₂O');
});
it('should get the proper gas label', () => {
// Testing for wrong capitalization of two word gas
const gasId = 'nitrous oxide';
const gasLabel = getGasLabel(gasId); // This should set to Nitrous Oxide before checking
expect(gasLabel).toBe('N₂O');
});
it('should get the proper gas label with a fallback', () => {
const gasId = 'nonexistent';
const gasLabel = getGasLabel(gasId, 'fallback');
expect(gasLabel).toBe('fallback');
});
it('should return none if no gas and no fallback is found', () => {
const gasId = 'nonexistent';
const gasLabel = getGasLabel(gasId);
expect(gasLabel).toBe('None');
});
it('should get the proper gas color', () => {
const gasId = 'nitrous_oxide';
const gasColor = getGasColor(gasId);
expect(gasColor).toBe('red');
});
it('should return a string if no gas is found', () => {
const gasId = 'nonexistent';
const gasColor = getGasColor(gasId);
expect(gasColor).toBe('black');
});
it('should return the gas object if found', () => {
const gasId = 'nitrous_oxide';
const gas = getGasFromId(gasId);
expect(gas).toEqual({
id: 'nitrous_oxide',
// path: '/datum/gas/antinoblium',
name: 'Nitrous Oxide',
label: 'N₂O',
color: 'red',
});
});
it('should return undefined if no gas is found', () => {
const gasId = 'nonexistent';
const gas = getGasFromId(gasId);
expect(gas).toBeUndefined();
});
/*
it('should return the gas using a path', () => {
const gasPath = '/datum/gas/antinoblium';
const gas = getGasFromPath(gasPath);
expect(gas).toEqual({
id: 'antinoblium',
path: '/datum/gas/antinoblium',
name: 'Antinoblium',
label: 'Anti-Noblium',
color: 'maroon',
});
});
*/
});

View File

@@ -1,316 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
type Gas = {
id: string;
// path: string;
name: string;
label: string;
color: string;
};
// VOREStation Addition start
/** 0.0 Degrees Celsius in Kelvin */
export const T0C = 273.15;
// VOREStation Addition end
// UI states, which are mirrored from the BYOND code.
export const UI_INTERACTIVE = 2;
export const UI_UPDATE = 1;
export const UI_DISABLED = 0;
export const UI_CLOSE = -1;
// All game related colors are stored here
export const COLORS = {
// Department colors
department: {
captain: '#c06616',
security: '#e74c3c',
medbay: '#3498db',
science: '#9b59b6',
engineering: '#f1c40f',
cargo: '#f39c12',
centcom: '#00c100',
other: '#c38312',
},
// VOREStation Addition begin
manifest: {
command: '#3333FF',
security: '#8e0000',
medical: '#006600',
engineering: '#b27300',
science: '#a65ba6',
cargo: '#bb9040',
planetside: '#555555',
civilian: '#a32800',
miscellaneous: '#666666',
silicon: '#222222',
},
// VOREStation Addition end
// Damage type colors
damageType: {
oxy: '#3498db',
toxin: '#2ecc71',
burn: '#e67e22',
brute: '#e74c3c',
},
// reagent / chemistry related colours
reagent: {
acidicbuffer: '#fbc314',
basicbuffer: '#3853a4',
},
} as const;
// Colors defined in CSS
export const CSS_COLORS = [
'black',
'white',
'red',
'orange',
'yellow',
'olive',
'green',
'teal',
'blue',
'violet',
'purple',
'pink',
'brown',
'grey',
'good',
'average',
'bad',
'label',
];
// VOREStation Edit Start
// If you ever add a new radio channel, you can either manually update this, or
// go use /client/verb/generate_tgui_radio_constants() in communications.dm.
export const RADIO_CHANNELS = [
{
'name': 'Mercenary',
'freq': 1213,
'color': '#6D3F40',
},
{
'name': 'Raider',
'freq': 1277,
'color': '#6D3F40',
},
{
'name': 'Special Ops',
'freq': 1341,
'color': '#5C5C8A',
},
{
'name': 'AI Private',
'freq': 1343,
'color': '#FF00FF',
},
{
'name': 'Response Team',
'freq': 1345,
'color': '#5C5C8A',
},
{
'name': 'Supply',
'freq': 1347,
'color': '#5F4519',
},
{
'name': 'Service',
'freq': 1349,
'color': '#6eaa2c',
},
{
'name': 'Science',
'freq': 1351,
'color': '#993399',
},
{
'name': 'Command',
'freq': 1353,
'color': '#193A7A',
},
{
'name': 'Medical',
'freq': 1355,
'color': '#008160',
},
{
'name': 'Engineering',
'freq': 1357,
'color': '#A66300',
},
{
'name': 'Security',
'freq': 1359,
'color': '#A30000',
},
{
'name': 'Explorer',
'freq': 1361,
'color': '#555555',
},
{
'name': 'Talon',
'freq': 1363,
'color': '#555555',
},
{
'name': 'Common',
'freq': 1459,
'color': '#008000',
},
{
'name': 'Entertainment',
'freq': 1461,
'color': '#339966',
},
{
'name': 'Security(I)',
'freq': 1475,
'color': '#008000',
},
{
'name': 'Medical(I)',
'freq': 1485,
'color': '#008000',
},
] as const;
/*
Entries must match /code/defines/gases.dm entries.
*/
const GASES = [
{
'id': 'oxygen',
'name': 'Oxygen',
'label': 'O₂',
'color': 'blue',
},
{
'id': 'nitrogen',
'name': 'Nitrogen',
'label': 'N₂',
'color': 'green',
},
{
'id': 'carbon_dioxide',
'name': 'Carbon Dioxide',
'label': 'CO₂',
'color': 'grey',
},
{
'id': 'phoron',
'name': 'Phoron',
'label': 'Phoron',
'color': 'pink',
},
{
'id': 'volatile_fuel',
'name': 'Volatile Fuel',
'label': 'EXP',
'color': 'teal',
},
{
'id': 'nitrous_oxide',
'name': 'Nitrous Oxide',
'label': 'N₂O',
'color': 'red',
},
{
'id': 'other',
'name': 'Other',
'label': 'Other',
'color': 'white',
},
{
'id': 'pressure',
'name': 'Pressure',
'label': 'Pressure',
'color': 'average',
},
{
'id': 'temperature',
'name': 'Temperature',
'label': 'Temperature',
'color': 'yellow',
},
] as const;
// VOREStation Edit End
// Returns gas label based on gasId
// Checks GASES for both id (all chars lowercase)
// and name (each word start capitalized, to match standards in code\defines\gases.dm)
export const getGasLabel = (gasId: string, fallbackValue?: string) => {
if (!gasId) return fallbackValue || 'None';
const gasSearchId = gasId.toLowerCase();
const gasSearchName = gasId.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
letter.toUpperCase()
);
for (let idx = 0; idx < GASES.length; idx++) {
if (GASES[idx].id === gasSearchId || GASES[idx].name === gasSearchName) {
return GASES[idx].label;
}
}
return fallbackValue || 'None';
};
// Returns gas color based on gasId
// Checks GASES for both id (all chars lowercase)
// and name (each word start capitalized, to match standards in code\defines\gases.dm)
export const getGasColor = (gasId: string) => {
if (!gasId) return 'black';
const gasSearchId = gasId.toLowerCase();
const gasSearchName = gasId.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
letter.toUpperCase()
);
for (let idx = 0; idx < GASES.length; idx++) {
if (GASES[idx].id === gasSearchId || GASES[idx].name === gasSearchName) {
return GASES[idx].color;
}
}
return 'black';
};
// Returns gas object based on gasId
// Checks GASES for both id (all chars lowercase)
// and name (each word start capitalized, to match standards in code\defines\gases.dm)
export const getGasFromId = (gasId: string): Gas | undefined => {
if (!gasId) return;
const gasSearchId = gasId.toLowerCase();
const gasSearchName = gasId.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
letter.toUpperCase()
);
for (let idx = 0; idx < GASES.length; idx++) {
if (GASES[idx].id === gasSearchId || GASES[idx].name === gasSearchName) {
return GASES[idx];
}
}
};
/*
// Returns gas object based on gasPath
export const getGasFromPath = (gasPath: string): Gas | undefined => {
if (!gasPath) return;
for (let idx = 0; idx < GASES.length; idx++) {
if (GASES[idx].path === gasPath) {
return GASES[idx];
}
}
};
*/

View File

@@ -1,54 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { useLocalState } from '../backend';
import { Flex, Section, Tabs } from '../components';
import { Pane, Window } from '../layouts';
const r = require.context('../stories', false, /\.stories\.js$/);
/**
* @returns {{
* meta: {
* title: string,
* render: () => any,
* },
* }[]}
*/
const getStories = () => r.keys().map((path) => r(path));
export const KitchenSink = (props, context) => {
const { panel } = props;
const [theme] = useLocalState(context, 'kitchenSinkTheme');
const [pageIndex, setPageIndex] = useLocalState(context, 'pageIndex', 0);
const stories = getStories();
const story = stories[pageIndex];
const Layout = panel ? Pane : Window;
return (
<Layout title="Kitchen Sink" width={600} height={500} theme={theme}>
<Flex height="100%">
<Flex.Item m={1} mr={0}>
<Section fill fitted>
<Tabs vertical>
{stories.map((story, i) => (
<Tabs.Tab
key={i}
color="transparent"
selected={i === pageIndex}
onClick={() => setPageIndex(i)}>
{story.meta.title}
</Tabs.Tab>
))}
</Tabs>
</Section>
</Flex.Item>
<Flex.Item position="relative" grow={1}>
<Layout.Content scrollable>{story.meta.render()}</Layout.Content>
</Flex.Item>
</Flex>
</Layout>
);
};

View File

@@ -1,11 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { createAction } from 'common/redux';
export const toggleKitchenSink = createAction('debug/toggleKitchenSink');
export const toggleDebugLayout = createAction('debug/toggleDebugLayout');
export const openExternalBrowser = createAction('debug/openExternalBrowser');

View File

@@ -1,10 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { useSelector } from 'common/redux';
import { selectDebug } from './selectors';
export const useDebug = (context) => useSelector(context, selectDebug);

View File

@@ -1,10 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export { useDebug } from './hooks';
export { KitchenSink } from './KitchenSink';
export { debugMiddleware, relayMiddleware } from './middleware';
export { debugReducer } from './reducer';

View File

@@ -1,81 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { KEY_BACKSPACE, KEY_F10, KEY_F11, KEY_F12 } from 'common/keycodes';
import { globalEvents } from '../events';
import { acquireHotKey } from '../hotkeys';
import { openExternalBrowser, toggleDebugLayout, toggleKitchenSink } from './actions';
// prettier-ignore
const relayedTypes = [
'backend/update',
'chat/message',
];
export const debugMiddleware = (store) => {
acquireHotKey(KEY_F11);
acquireHotKey(KEY_F12);
globalEvents.on('keydown', (key) => {
if (key.code === KEY_F11) {
store.dispatch(toggleDebugLayout());
}
if (key.code === KEY_F12) {
store.dispatch(toggleKitchenSink());
}
if (key.ctrl && key.alt && key.code === KEY_BACKSPACE) {
// NOTE: We need to call this in a timeout, because we need a clean
// stack in order for this to be a fatal error.
setTimeout(() => {
// prettier-ignore
throw new Error(
'OOPSIE WOOPSIE!! UwU We made a fucky wucky!! A wittle'
+ ' fucko boingo! The code monkeys at our headquarters are'
+ ' working VEWY HAWD to fix this!');
});
}
});
return (next) => (action) => next(action);
};
export const relayMiddleware = (store) => {
const devServer = require('tgui-dev-server/link/client.cjs');
const externalBrowser = location.search === '?external';
if (externalBrowser) {
devServer.subscribe((msg) => {
const { type, payload } = msg;
if (type === 'relay' && payload.windowId === Byond.windowId) {
store.dispatch({
...payload.action,
relayed: true,
});
}
});
} else {
acquireHotKey(KEY_F10);
globalEvents.on('keydown', (key) => {
if (key === KEY_F10) {
store.dispatch(openExternalBrowser());
}
});
}
return (next) => (action) => {
const { type, payload, relayed } = action;
if (type === openExternalBrowser.type) {
window.open(location.href + '?external', '_blank');
return;
}
if (relayedTypes.includes(type) && !relayed && !externalBrowser) {
devServer.sendMessage({
type: 'relay',
payload: {
windowId: Byond.windowId,
action,
},
});
}
return next(action);
};
};

View File

@@ -1,22 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const debugReducer = (state = {}, action) => {
const { type, payload } = action;
if (type === 'debug/toggleKitchenSink') {
return {
...state,
kitchenSink: !state.kitchenSink,
};
}
if (type === 'debug/toggleDebugLayout') {
return {
...state,
debugLayout: !state.debugLayout,
};
}
return state;
};

View File

@@ -1,7 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
export const selectDebug = (state) => state.debug;

View File

@@ -1,286 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'common/vector';
import { createLogger } from './logging';
import { storage } from 'common/storage';
const logger = createLogger('drag');
const pixelRatio = window.devicePixelRatio ?? 1;
let windowKey = Byond.windowId;
let dragging = false;
let resizing = false;
let screenOffset: [number, number] = [0, 0];
let screenOffsetPromise: Promise<[number, number]>;
let dragPointOffset: [number, number];
let resizeMatrix: [number, number];
let initialSize: [number, number];
let size: [number, number];
// Set the window key
export const setWindowKey = (key: string): void => {
windowKey = key;
};
// Get window position
export const getWindowPosition = (): [number, number] => [
window.screenLeft * pixelRatio,
window.screenTop * pixelRatio,
];
// Get window size
export const getWindowSize = (): [number, number] => [
window.innerWidth * pixelRatio,
window.innerHeight * pixelRatio,
];
// Set window position
const setWindowPosition = (vec: [number, number]) => {
const byondPos = vecAdd(vec, screenOffset);
return Byond.winset(Byond.windowId, {
pos: byondPos[0] + ',' + byondPos[1],
});
};
// Set window size
const setWindowSize = (vec: [number, number]) => {
return Byond.winset(Byond.windowId, {
size: vec[0] + 'x' + vec[1],
});
};
// Get screen position
const getScreenPosition = (): [number, number] => [
0 - screenOffset[0],
0 - screenOffset[1],
];
// Get screen size
const getScreenSize = (): [number, number] => [
window.screen.availWidth * pixelRatio,
window.screen.availHeight * pixelRatio,
];
/**
* Moves an item to the top of the recents array, and keeps its length
* limited to the number in `limit` argument.
*
* Uses a strict equality check for comparisons.
*
* Returns new recents and an item which was trimmed.
*/
export const touchRecents = (
recents: string[],
touchedItem: string,
limit = 50
): [string[], string | undefined] => {
const nextRecents: string[] = [touchedItem];
let trimmedItem: string | undefined;
for (let i = 0; i < recents.length; i++) {
const item = recents[i];
if (item === touchedItem) {
continue;
}
if (nextRecents.length < limit) {
nextRecents.push(item);
} else {
trimmedItem = item;
}
}
return [nextRecents, trimmedItem];
};
// Store window geometry in local storage
const storeWindowGeometry = async () => {
logger.log('storing geometry');
const geometry = {
pos: getWindowPosition(),
size: getWindowSize(),
};
storage.set(windowKey, geometry);
// Update the list of stored geometries
const [geometries, trimmedKey] = touchRecents(
(await storage.get('geometries')) || [],
windowKey
);
if (trimmedKey) {
storage.remove(trimmedKey);
}
storage.set('geometries', geometries);
};
// Recall window geometry from local storage and apply it
export const recallWindowGeometry = async (
options: {
fancy?: boolean;
pos?: [number, number];
size?: [number, number];
locked?: boolean;
} = {}
) => {
const geometry = options.fancy && (await storage.get(windowKey));
if (geometry) {
logger.log('recalled geometry:', geometry);
}
// options.pos is assumed to already be in display-pixels
let pos = geometry?.pos || options.pos;
let size = options.size;
// Convert size from css-pixels to display-pixels
if (size) {
size = [size[0] * pixelRatio, size[1] * pixelRatio];
}
// Wait until screen offset gets resolved
await screenOffsetPromise;
const areaAvailable = getScreenSize();
// Set window size
if (size) {
// Constraint size to not exceed available screen area
size = [
Math.min(areaAvailable[0], size[0]),
Math.min(areaAvailable[1], size[1]),
];
setWindowSize(size);
}
// Set window position
if (pos) {
// Constraint window position if monitor lock was set in preferences.
if (size && options.locked) {
pos = constraintPosition(pos, size)[1];
}
setWindowPosition(pos);
// Set window position at the center of the screen.
} else if (size) {
pos = vecAdd(
vecScale(areaAvailable, 0.5),
vecScale(size, -0.5),
vecScale(screenOffset, -1.0)
);
setWindowPosition(pos);
}
};
// Setup draggable window
export const setupDrag = async () => {
// Calculate screen offset caused by the windows taskbar
let windowPosition = getWindowPosition();
screenOffsetPromise = Byond.winget(Byond.windowId, 'pos').then((pos) => [
pos.x - windowPosition[0],
pos.y - windowPosition[1],
]);
screenOffset = await screenOffsetPromise;
logger.debug('screen offset', screenOffset);
};
/**
* Constraints window position to safe screen area, accounting for safe
* margins which could be a system taskbar.
*/
const constraintPosition = (
pos: [number, number],
size: [number, number]
): [boolean, [number, number]] => {
const screenPos = getScreenPosition();
const screenSize = getScreenSize();
const nextPos: [number, number] = [pos[0], pos[1]];
let relocated = false;
for (let i = 0; i < 2; i++) {
const leftBoundary = screenPos[i];
const rightBoundary = screenPos[i] + screenSize[i];
if (pos[i] < leftBoundary) {
nextPos[i] = leftBoundary;
relocated = true;
} else if (pos[i] + size[i] > rightBoundary) {
nextPos[i] = rightBoundary - size[i];
relocated = true;
}
}
return [relocated, nextPos];
};
// Start dragging the window
export const dragStartHandler = (event: MouseEvent) => {
logger.log('drag start');
dragging = true;
dragPointOffset = vecSubtract(
[event.screenX, event.screenY],
getWindowPosition()
);
// Focus click target
(event.target as HTMLElement)?.focus();
document.addEventListener('mousemove', dragMoveHandler);
document.addEventListener('mouseup', dragEndHandler);
dragMoveHandler(event);
};
// End dragging the window
const dragEndHandler = (event: MouseEvent) => {
logger.log('drag end');
dragMoveHandler(event);
document.removeEventListener('mousemove', dragMoveHandler);
document.removeEventListener('mouseup', dragEndHandler);
dragging = false;
storeWindowGeometry();
};
// Move the window while dragging
const dragMoveHandler = (event: MouseEvent) => {
if (!dragging) {
return;
}
event.preventDefault();
setWindowPosition(
vecSubtract([event.screenX, event.screenY], dragPointOffset)
);
};
// Start resizing the window
export const resizeStartHandler =
(x: number, y: number) => (event: MouseEvent) => {
resizeMatrix = [x, y];
logger.log('resize start', resizeMatrix);
resizing = true;
dragPointOffset = vecSubtract(
[event.screenX, event.screenY],
getWindowPosition()
);
initialSize = getWindowSize();
// Focus click target
(event.target as HTMLElement)?.focus();
document.addEventListener('mousemove', resizeMoveHandler);
document.addEventListener('mouseup', resizeEndHandler);
resizeMoveHandler(event);
};
// End resizing the window
const resizeEndHandler = (event: MouseEvent) => {
logger.log('resize end', size);
resizeMoveHandler(event);
document.removeEventListener('mousemove', resizeMoveHandler);
document.removeEventListener('mouseup', resizeEndHandler);
resizing = false;
storeWindowGeometry();
};
// Move the window while resizing
const resizeMoveHandler = (event: MouseEvent) => {
if (!resizing) {
return;
}
event.preventDefault();
const currentOffset = vecSubtract(
[event.screenX, event.screenY],
getWindowPosition()
);
const delta = vecSubtract(currentOffset, dragPointOffset);
// Extra 1x1 area is added to ensure the browser can see the cursor
size = vecAdd(initialSize, vecMultiply(resizeMatrix, delta), [1, 1]);
// Sane window size values
size[0] = Math.max(size[0], 150 * pixelRatio);
size[1] = Math.max(size[1], 50 * pixelRatio);
setWindowSize(size);
};

View File

@@ -1,60 +0,0 @@
import { KeyEvent, addScrollableNode, canStealFocus, removeScrollableNode, setupGlobalEvents } from './events';
describe('focusEvents', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('setupGlobalEvents sets the ignoreWindowFocus flag correctly', () => {
setupGlobalEvents({ ignoreWindowFocus: true });
// Test other functionality that depends on the ignoreWindowFocus flag
});
it('canStealFocus returns true for input and textarea elements', () => {
const inputElement = document.createElement('input');
const textareaElement = document.createElement('textarea');
const divElement = document.createElement('div');
expect(canStealFocus(inputElement)).toBe(true);
expect(canStealFocus(textareaElement)).toBe(true);
expect(canStealFocus(divElement)).toBe(false);
});
it('addScrollableNode and removeScrollableNode manage the list of scrollable nodes correctly', () => {
const divElement1 = document.createElement('div');
const divElement2 = document.createElement('div');
addScrollableNode(divElement1);
addScrollableNode(divElement2);
// Test other functionality that depends on the list of scrollable nodes
removeScrollableNode(divElement1);
removeScrollableNode(divElement2);
// Test other functionality that depends on the list of scrollable nodes
});
it('KeyEvent class works correctly', () => {
const keyboardEvent = new KeyboardEvent('keydown', {
key: 'a',
keyCode: 65,
ctrlKey: true,
altKey: true,
shiftKey: true,
});
const keyEvent = new KeyEvent(keyboardEvent, 'keydown', false);
expect(keyEvent.event).toBe(keyboardEvent);
expect(keyEvent.type).toBe('keydown');
expect(keyEvent.code).toBe(65);
expect(keyEvent.ctrl).toBe(true);
expect(keyEvent.alt).toBe(true);
expect(keyEvent.shift).toBe(true);
expect(keyEvent.repeat).toBe(false);
expect(keyEvent.hasModifierKeys()).toBe(true);
expect(keyEvent.isModifierKey()).toBe(false);
expect(keyEvent.isDown()).toBe(true);
expect(keyEvent.isUp()).toBe(false);
expect(keyEvent.toString()).toBe('Ctrl+Alt+Shift+A');
});
});

View File

@@ -1,233 +0,0 @@
/**
* Normalized browser focus events and BYOND-specific focus helpers.
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import { KEY_ALT, KEY_CTRL, KEY_F1, KEY_F12, KEY_SHIFT } from 'common/keycodes';
import { EventEmitter } from 'common/events';
export const globalEvents = new EventEmitter();
let ignoreWindowFocus = false;
export const setupGlobalEvents = (
options: { ignoreWindowFocus?: boolean } = {}
): void => {
ignoreWindowFocus = !!options.ignoreWindowFocus;
};
// Window focus
// --------------------------------------------------------
let windowFocusTimeout: ReturnType<typeof setTimeout> | null;
let windowFocused = true;
// Pretend to always be in focus.
const setWindowFocus = (value: boolean, delayed?: boolean) => {
if (ignoreWindowFocus) {
windowFocused = true;
return;
}
if (windowFocusTimeout) {
clearTimeout(windowFocusTimeout);
windowFocusTimeout = null;
}
if (delayed) {
windowFocusTimeout = setTimeout(() => setWindowFocus(value));
return;
}
if (windowFocused !== value) {
windowFocused = value;
globalEvents.emit(value ? 'window-focus' : 'window-blur');
globalEvents.emit('window-focus-change', value);
}
};
// Focus stealing
// --------------------------------------------------------
let focusStolenBy: HTMLElement | null = null;
export const canStealFocus = (node: HTMLElement) => {
const tag = String(node.tagName).toLowerCase();
return tag === 'input' || tag === 'textarea';
};
const stealFocus = (node: HTMLElement) => {
releaseStolenFocus();
focusStolenBy = node;
focusStolenBy.addEventListener('blur', releaseStolenFocus);
};
const releaseStolenFocus = () => {
if (focusStolenBy) {
focusStolenBy.removeEventListener('blur', releaseStolenFocus);
focusStolenBy = null;
}
};
// Focus follows the mouse
// --------------------------------------------------------
let focusedNode: HTMLElement | null = null;
let lastVisitedNode: HTMLElement | null = null;
const trackedNodes: HTMLElement[] = [];
export const addScrollableNode = (node: HTMLElement) => {
trackedNodes.push(node);
};
export const removeScrollableNode = (node: HTMLElement) => {
const index = trackedNodes.indexOf(node);
if (index >= 0) {
trackedNodes.splice(index, 1);
}
};
const focusNearestTrackedParent = (node: HTMLElement | null) => {
if (focusStolenBy || !windowFocused) {
return;
}
const body = document.body;
while (node && node !== body) {
if (trackedNodes.includes(node)) {
// NOTE: Contains is a DOM4 method
if (node.contains(focusedNode)) {
return;
}
focusedNode = node;
node.focus();
return;
}
node = node.parentElement;
}
};
window.addEventListener('mousemove', (e) => {
const node = e.target as HTMLElement;
if (node !== lastVisitedNode) {
lastVisitedNode = node;
focusNearestTrackedParent(node);
}
});
// Focus event hooks
// --------------------------------------------------------
window.addEventListener('focusin', (e) => {
lastVisitedNode = null;
focusedNode = e.target as HTMLElement;
setWindowFocus(true);
if (canStealFocus(e.target as HTMLElement)) {
stealFocus(e.target as HTMLElement);
return;
}
});
window.addEventListener('focusout', (e) => {
lastVisitedNode = null;
setWindowFocus(false, true);
});
window.addEventListener('blur', (e) => {
lastVisitedNode = null;
setWindowFocus(false, true);
});
window.addEventListener('beforeunload', (e) => {
setWindowFocus(false);
});
// Key events
// --------------------------------------------------------
const keyHeldByCode: Record<number, boolean> = {};
export class KeyEvent {
event: KeyboardEvent;
type: 'keydown' | 'keyup';
code: number;
ctrl: boolean;
shift: boolean;
alt: boolean;
repeat: boolean;
_str?: string;
constructor(e: KeyboardEvent, type: 'keydown' | 'keyup', repeat?: boolean) {
this.event = e;
this.type = type;
this.code = e.keyCode;
this.ctrl = e.ctrlKey;
this.shift = e.shiftKey;
this.alt = e.altKey;
this.repeat = !!repeat;
}
hasModifierKeys() {
return this.ctrl || this.alt || this.shift;
}
isModifierKey() {
return (
this.code === KEY_CTRL || this.code === KEY_SHIFT || this.code === KEY_ALT
);
}
isDown() {
return this.type === 'keydown';
}
isUp() {
return this.type === 'keyup';
}
toString() {
if (this._str) {
return this._str;
}
this._str = '';
if (this.ctrl) {
this._str += 'Ctrl+';
}
if (this.alt) {
this._str += 'Alt+';
}
if (this.shift) {
this._str += 'Shift+';
}
if (this.code >= 48 && this.code <= 90) {
this._str += String.fromCharCode(this.code);
} else if (this.code >= KEY_F1 && this.code <= KEY_F12) {
this._str += 'F' + (this.code - 111);
} else {
this._str += '[' + this.code + ']';
}
return this._str;
}
}
// IE8: Keydown event is only available on document.
document.addEventListener('keydown', (e) => {
if (canStealFocus(e.target as HTMLElement)) {
return;
}
const code = e.keyCode;
const key = new KeyEvent(e, 'keydown', keyHeldByCode[code]);
globalEvents.emit('keydown', key);
globalEvents.emit('key', key);
keyHeldByCode[code] = true;
});
document.addEventListener('keyup', (e) => {
if (canStealFocus(e.target as HTMLElement)) {
return;
}
const code = e.keyCode;
const key = new KeyEvent(e, 'keyup');
globalEvents.emit('keyup', key);
globalEvents.emit('key', key);
keyHeldByCode[code] = false;
});

View File

@@ -1,25 +0,0 @@
/**
* Various focus helpers.
*
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
/**
* Moves focus to the BYOND map window.
*/
export const focusMap = () => {
Byond.winset('mapwindow.map', {
focus: true,
});
};
/**
* Moves focus to the browser window.
*/
export const focusWindow = () => {
Byond.winset(Byond.windowId, {
focus: true,
});
};

View File

@@ -1,112 +0,0 @@
import { formatDb, formatMoney, formatSiBaseTenUnit, formatSiUnit, formatTime } from './format';
describe('formatSiUnit', () => {
it('formats base values correctly', () => {
const value = 100;
const result = formatSiUnit(value);
expect(result).toBe('100');
});
it('formats kilo values correctly', () => {
const value = 1500;
const result = formatSiUnit(value);
expect(result).toBe('1.50 k');
});
it('formats micro values correctly', () => {
const value = 0.0001;
const result = formatSiUnit(value);
expect(result).toBe('100 μ');
});
it('formats values with custom units correctly', () => {
const value = 0.5;
const result = formatSiUnit(value, 0, 'Hz');
expect(result).toBe('0.50 Hz');
});
it('handles non-finite values correctly', () => {
const value = Infinity;
const result = formatSiUnit(value);
expect(result).toBe('Infinity');
});
});
describe('formatMoney', () => {
it('formats integer values with default precision', () => {
const value = 1234567;
const result = formatMoney(value);
expect(result).toBe('1\u2009234\u2009567');
});
it('formats float values with specified precision', () => {
const value = 1234567.89;
const result = formatMoney(value, 2);
expect(result).toBe('1\u2009234\u2009567.89');
});
it('formats negative values correctly', () => {
const value = -1234567.89;
const result = formatMoney(value, 2);
expect(result).toBe('-1\u2009234\u2009567.89');
});
it('returns non-finite values as is', () => {
const value = Infinity;
const result = formatMoney(value);
expect(result).toBe('Infinity');
});
it('formats zero correctly', () => {
const value = 0;
const result = formatMoney(value);
expect(result).toBe('0');
});
});
describe('formatDb', () => {
it('formats positive values correctly', () => {
const value = 1;
const result = formatDb(value);
expect(result).toBe('+0.00 dB');
});
it('formats negative values correctly', () => {
const value = 0.5;
const result = formatDb(value);
expect(result).toBe('-6.02 dB');
});
it('formats Infinity correctly', () => {
const value = 0;
const result = formatDb(value);
expect(result).toBe('-Inf dB');
});
it('formats very large values correctly', () => {
const value = 1e6;
const result = formatDb(value);
expect(result).toBe('+120.00 dB');
});
it('formats very small values correctly', () => {
const value = 1e-6;
const result = formatDb(value);
expect(result).toBe('-120.00 dB');
});
});
describe('formatSiBaseTenUnit', () => {
it('formats SI base 10 units', () => {
expect(formatSiBaseTenUnit(1e9)).toBe('1.00 · 10⁹');
expect(formatSiBaseTenUnit(1234567890, 0, 'm')).toBe('1.23 · 10⁹ m');
});
});
describe('formatTime', () => {
it('formats time values', () => {
expect(formatTime(36000)).toBe('01:00:00');
expect(formatTime(36610)).toBe('01:01:01');
expect(formatTime(36610, 'short')).toBe('1h1m1s');
});
});

View File

@@ -1,181 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
const SI_SYMBOLS = [
'f', // femto
'p', // pico
'n', // nano
'μ', // micro
'm', // milli
// NOTE: This is a space for a reason. When we right align si numbers,
// in monospace mode, we want to units and numbers stay in their respective
// columns. If rendering in HTML mode, this space will collapse into
// a single space anyway.
' ', // base
'k', // kilo
'M', // mega
'G', // giga
'T', // tera
'P', // peta
'E', // exa
'Z', // zetta
'Y', // yotta
'R', // ronna
'Q', // quecca
'F',
'N',
'H',
] as const;
const SI_BASE_INDEX = SI_SYMBOLS.indexOf(' ');
// Formats a number to a human readable form, with a custom unit
export const formatSiUnit = (
value: number,
minBase1000 = -SI_BASE_INDEX,
unit = ''
): string => {
if (!isFinite(value)) {
return value.toString();
}
const realBase10 = Math.floor(Math.log10(Math.abs(value)));
const base10 = Math.max(minBase1000 * 3, realBase10);
const base1000 = Math.floor(base10 / 3);
const symbol =
SI_SYMBOLS[Math.min(base1000 + SI_BASE_INDEX, SI_SYMBOLS.length - 1)];
const scaledValue = value / Math.pow(1000, base1000);
let formattedValue = scaledValue.toFixed(2);
if (formattedValue.endsWith('.00')) {
formattedValue = formattedValue.slice(0, -3);
} else if (formattedValue.endsWith('.0')) {
formattedValue = formattedValue.slice(0, -2);
}
return `${formattedValue} ${symbol.trim()}${unit}`.trim();
};
// Formats a number to a human readable form, with power (W) as the unit
export const formatPower = (value: number, minBase1000 = 0) => {
return formatSiUnit(value, minBase1000, 'W');
};
// Formats a number as a currency string
export const formatMoney = (value: number, precision = 0) => {
if (!Number.isFinite(value)) {
return String(value);
}
// Round the number and make it fixed precision
const roundedValue = Number(value.toFixed(precision));
// Handle the negative sign
const isNegative = roundedValue < 0;
const absoluteValue = Math.abs(roundedValue);
// Convert to string and place thousand separators
const parts = absoluteValue.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '\u2009'); // Thin space
const formattedValue = parts.join('.');
return isNegative ? `-${formattedValue}` : formattedValue;
};
// Formats a floating point number as a number on the decibel scale
export const formatDb = (value: number) => {
const db = 20 * Math.log10(value);
const sign = db >= 0 ? '+' : '-';
let formatted: string | number = Math.abs(db);
if (formatted === Infinity) {
formatted = 'Inf';
} else {
formatted = formatted.toFixed(2);
}
return `${sign}${formatted} dB`;
};
const SI_BASE_TEN_UNITS = [
'',
'· 10³', // kilo
'· 10⁶', // mega
'· 10⁹', // giga
'· 10¹²', // tera
'· 10¹⁵', // peta
'· 10¹⁸', // exa
'· 10²¹', // zetta
'· 10²⁴', // yotta
'· 10²⁷', // ronna
'· 10³⁰', // quecca
'· 10³³',
'· 10³⁶',
'· 10³⁹',
] as const;
// Converts a number to a string with SI base 10 units
export const formatSiBaseTenUnit = (
value: number,
minBase1000 = 0,
unit = ''
): string => {
if (!isFinite(value)) {
return 'NaN';
}
const realBase10 = Math.floor(Math.log10(value));
const base10 = Math.max(minBase1000 * 3, realBase10);
const base1000 = Math.floor(base10 / 3);
const symbol = SI_BASE_TEN_UNITS[base1000];
const scaledValue = value / Math.pow(1000, base1000);
const precision = Math.max(0, 2 - (base10 % 3));
const formattedValue = scaledValue.toFixed(precision);
return `${formattedValue} ${symbol} ${unit}`.trim();
};
/**
* Formats decisecond count into HH:MM:SS display by default
* "short" format does not pad and adds hms suffixes
*/
export const formatTime = (
val: number,
formatType: 'short' | 'default' = 'default'
): string => {
const totalSeconds = Math.floor(val / 10);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (formatType === 'short') {
const hoursFormatted = hours > 0 ? `${hours}h` : '';
const minutesFormatted = minutes > 0 ? `${minutes}m` : '';
const secondsFormatted = seconds > 0 ? `${seconds}s` : '';
return `${hoursFormatted}${minutesFormatted}${secondsFormatted}`;
}
const hoursPadded = String(hours).padStart(2, '0');
const minutesPadded = String(minutes).padStart(2, '0');
const secondsPadded = String(seconds).padStart(2, '0');
return `${hoursPadded}:${minutesPadded}:${secondsPadded}`;
};
/* VOREStation Addition Start */
export const formatCommaNumber = (value) => {
if (!Number.isFinite(value)) {
return value;
}
// From http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
let parts = value.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
};
/* VOREStation Addition End */

View File

@@ -1,220 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
import * as keycodes from 'common/keycodes';
import { globalEvents, KeyEvent } from './events';
import { createLogger } from './logging';
const logger = createLogger('hotkeys');
// BYOND macros, in `key: command` format.
const byondMacros: Record<string, string> = {};
// Default set of acquired keys, which will not be sent to BYOND.
const hotKeysAcquired = [
keycodes.KEY_ESCAPE,
keycodes.KEY_ENTER,
keycodes.KEY_SPACE,
keycodes.KEY_TAB,
keycodes.KEY_CTRL,
keycodes.KEY_SHIFT,
keycodes.KEY_UP,
keycodes.KEY_DOWN,
keycodes.KEY_LEFT,
keycodes.KEY_RIGHT,
keycodes.KEY_F5,
];
// State of passed-through keys.
const keyState: Record<string, boolean> = {};
// Custom listeners for key events
const keyListeners: ((key: KeyEvent) => void)[] = [];
/**
* Converts a browser keycode to BYOND keycode.
*/
const keyCodeToByond = (keyCode: number) => {
if (keyCode === 16) return 'Shift';
if (keyCode === 17) return 'Ctrl';
if (keyCode === 18) return 'Alt';
if (keyCode === 33) return 'Northeast';
if (keyCode === 34) return 'Southeast';
if (keyCode === 35) return 'Southwest';
if (keyCode === 36) return 'Northwest';
if (keyCode === 37) return 'West';
if (keyCode === 38) return 'North';
if (keyCode === 39) return 'East';
if (keyCode === 40) return 'South';
if (keyCode === 45) return 'Insert';
if (keyCode === 46) return 'Delete';
// prettier-ignore
if (keyCode >= 48 && keyCode <= 57 || keyCode >= 65 && keyCode <= 90) {
return String.fromCharCode(keyCode);
}
if (keyCode >= 96 && keyCode <= 105) {
return 'Numpad' + (keyCode - 96);
}
if (keyCode >= 112 && keyCode <= 123) {
return 'F' + (keyCode - 111);
}
if (keyCode === 188) return ',';
if (keyCode === 189) return '-';
if (keyCode === 190) return '.';
};
/**
* Keyboard passthrough logic. This allows you to keep doing things
* in game while the browser window is focused.
*/
const handlePassthrough = (key: KeyEvent) => {
const keyString = String(key);
// In addition to F5, support reloading with Ctrl+R and Ctrl+F5
if (keyString === 'Ctrl+F5' || keyString === 'Ctrl+R') {
location.reload();
return;
}
// Prevent passthrough on Ctrl+F
if (keyString === 'Ctrl+F') {
return;
}
// NOTE: Alt modifier is pretty bad and sticky in IE11.
// prettier-ignore
if (
key.event.defaultPrevented
|| key.isModifierKey()
|| hotKeysAcquired.includes(key.code)
) {
return;
}
const byondKeyCode = keyCodeToByond(key.code);
if (!byondKeyCode) {
return;
}
// Macro
const macro = byondMacros[byondKeyCode];
if (macro) {
logger.debug('macro', macro);
return Byond.command(macro);
}
// KeyDown
if (key.isDown() && !keyState[byondKeyCode]) {
keyState[byondKeyCode] = true;
const command = `KeyDown "${byondKeyCode}"`;
logger.debug(command);
return Byond.command(command);
}
// KeyUp
if (key.isUp() && keyState[byondKeyCode]) {
keyState[byondKeyCode] = false;
const command = `KeyUp "${byondKeyCode}"`;
logger.debug(command);
return Byond.command(command);
}
};
/**
* Acquires a lock on the hotkey, which prevents it from being
* passed through to BYOND.
*/
export const acquireHotKey = (keyCode: number) => {
hotKeysAcquired.push(keyCode);
};
/**
* Makes the hotkey available to BYOND again.
*/
export const releaseHotKey = (keyCode: number) => {
const index = hotKeysAcquired.indexOf(keyCode);
if (index >= 0) {
hotKeysAcquired.splice(index, 1);
}
};
export const releaseHeldKeys = () => {
for (let byondKeyCode of Object.keys(keyState)) {
if (keyState[byondKeyCode]) {
keyState[byondKeyCode] = false;
logger.log(`releasing key "${byondKeyCode}"`);
Byond.command(`KeyUp "${byondKeyCode}"`);
}
}
};
type ByondSkinMacro = {
command: string;
name: string;
};
export const setupHotKeys = () => {
// Read macros
Byond.winget('default.*').then((data: Record<string, string>) => {
// Group each macro by ref
const groupedByRef: Record<string, ByondSkinMacro> = {};
for (let key of Object.keys(data)) {
const keyPath = key.split('.');
const ref = keyPath[1];
const prop = keyPath[2];
if (ref && prop) {
// This piece of code imperatively adds each property to a
// ByondSkinMacro object in the order we meet it, which is hard
// to express safely in typescript.
if (!groupedByRef[ref]) {
groupedByRef[ref] = {} as any;
}
groupedByRef[ref][prop] = data[key];
}
}
// Insert macros
const escapedQuotRegex = /\\"/g;
// prettier-ignore
const unescape = (str: string) => str
.substring(1, str.length - 1)
.replace(escapedQuotRegex, '"');
for (let ref of Object.keys(groupedByRef)) {
const macro = groupedByRef[ref];
const byondKeyName = unescape(macro.name);
byondMacros[byondKeyName] = unescape(macro.command);
}
logger.debug('loaded macros', byondMacros);
});
// Setup event handlers
globalEvents.on('window-blur', () => {
releaseHeldKeys();
});
globalEvents.on('key', (key: KeyEvent) => {
for (const keyListener of keyListeners) {
keyListener(key);
}
handlePassthrough(key);
});
};
/**
* Registers for any key events, such as key down or key up.
* This should be preferred over directly connecting to keydown/keyup
* as it lets tgui prevent the key from reaching BYOND.
*
* If using in a component, prefer KeyListener, which automatically handles
* stopping listening when unmounting.
*
* @param callback The function to call whenever a key event occurs
* @returns A callback to stop listening
*/
export const listenForKeyEvents = (callback: (key: KeyEvent) => void) => {
keyListeners.push(callback);
let removed = false;
return () => {
if (removed) {
return;
}
removed = true;
keyListeners.splice(keyListeners.indexOf(callback), 1);
};
};

View File

@@ -1,16 +0,0 @@
/**
* An equivalent to `fetch`, except will automatically retry.
*/
export const fetchRetry = (
url: string,
options?: RequestInit,
retryTimer: number = 1000
): Promise<Response> => {
return fetch(url, options).catch(() => {
return new Promise((resolve) => {
setTimeout(() => {
fetchRetry(url, options, retryTimer).then(resolve);
}, retryTimer);
});
});
};

View File

@@ -1,78 +0,0 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
// Themes
import './styles/main.scss';
import './styles/themes/abductor.scss';
import './styles/themes/cardtable.scss';
import './styles/themes/hackerman.scss';
import './styles/themes/malfunction.scss';
import './styles/themes/neutral.scss';
import './styles/themes/ntos.scss';
import './styles/themes/paper.scss';
import './styles/themes/pda-retro.scss';
import './styles/themes/retro.scss';
import './styles/themes/syndicate.scss';
import './styles/themes/wizard.scss';
import './styles/themes/abstract.scss';
import { StoreProvider, configureStore } from './store';
import { captureExternalLinks } from './links';
import { createRenderer } from './renderer';
import { perf } from 'common/perf';
import { setupGlobalEvents } from './events';
import { setupHotKeys } from './hotkeys';
import { setupHotReloading } from 'tgui-dev-server/link/client.cjs';
perf.mark('inception', window.performance?.timing?.navigationStart);
perf.mark('init');
const store = configureStore();
const renderApp = createRenderer(() => {
const { getRoutedComponent } = require('./routes');
const Component = getRoutedComponent(store);
return (
<StoreProvider store={store}>
<Component />
</StoreProvider>
);
});
const setupApp = () => {
// Delay setup
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupApp);
return;
}
setupGlobalEvents();
setupHotKeys();
captureExternalLinks();
// Re-render UI on store updates
store.subscribe(renderApp);
// Dispatch incoming messages as store actions
Byond.subscribe((type, payload) => store.dispatch({ type, payload }));
// Enable hot module reloading
if (module.hot) {
setupHotReloading();
// prettier-ignore
module.hot.accept([
'./components',
'./debug',
'./layouts',
'./routes',
], () => {
renderApp();
});
}
};
setupApp();

View File

@@ -1,126 +0,0 @@
import { useBackend } from '../backend';
import { Button, ProgressBar, LabeledList, Box, Section } from '../components';
import { Window } from '../layouts';
export const AICard = (props, context) => {
const { act, data } = useBackend(context);
const {
has_ai,
integrity,
backup_capacitor,
flushing,
has_laws,
laws,
wireless,
radio,
} = data;
if (has_ai === 0) {
return (
<Window width={600} height={470} resizable>
<Window.Content>
<Section title="Stored AI">
<Box>
<h3>No AI detected.</h3>
</Box>
</Section>
</Window.Content>
</Window>
);
} else {
let integrityColor = null; // Handles changing color of the integrity bar
if (integrity >= 75) {
integrityColor = 'green';
} else if (integrity >= 25) {
integrityColor = 'yellow';
} else {
integrityColor = 'red';
}
let powerColor = null;
if (backup_capacitor >= 75) {
powerColor = 'green';
}
if (backup_capacitor >= 25) {
powerColor = 'yellow';
} else {
powerColor = 'red';
}
return (
<Window width={600} height={470} resizable>
<Window.Content scrollable>
<Section title="Stored AI">
<Box bold display="inline-block">
<h3>{name}</h3>
</Box>
<Box>
<LabeledList>
<LabeledList.Item label="Integrity">
<ProgressBar color={integrityColor} value={integrity / 100} />
</LabeledList.Item>
<LabeledList.Item label="Power">
<ProgressBar
color={powerColor}
value={backup_capacitor / 100}
/>
</LabeledList.Item>
</LabeledList>
</Box>
<Box color="red">
<h2>{flushing === 1 ? 'Wipe of AI in progress...' : ''}</h2>
</Box>
</Section>
<Section title="Laws">
{(!!has_laws && (
<Box>
{laws.map((value, key) => (
<Box key={key} display="inline-block">
{value}
</Box>
))}
</Box>
)) || ( // Else, no laws.
<Box color="red">
<h3>No laws detected.</h3>
</Box>
)}
</Section>
<Section title="Actions">
<LabeledList>
<LabeledList.Item label="Wireless Activity">
<Button
icon={wireless ? 'check' : 'times'}
content={wireless ? 'Enabled' : 'Disabled'}
color={wireless ? 'green' : 'red'}
onClick={() => act('wireless')}
/>
</LabeledList.Item>
<LabeledList.Item label="Subspace Transceiver">
<Button
icon={radio ? 'check' : 'times'}
content={radio ? 'Enabled' : 'Disabled'}
color={radio ? 'green' : 'red'}
onClick={() => act('radio')}
/>
</LabeledList.Item>
<LabeledList.Item label="AI Power">
<Button.Confirm
icon="radiation"
confirmIcon="radiation"
disabled={flushing || integrity === 0}
confirmColor="red"
content="Shutdown"
onClick={() => act('wipe')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
}
};

View File

@@ -1,311 +0,0 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, Dimmer, Icon, LabeledList, ProgressBar, Section } from '../components';
import { Window } from '../layouts';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
import { FullscreenNotice } from './common/FullscreenNotice';
export const APC = (props, context) => {
const { act, data } = useBackend(context);
let body = <ApcContent />;
if (data.gridCheck) {
body = <GridCheck />;
} else if (data.failTime) {
body = <ApcFailure />;
}
return (
<Window width={450} height={475} resizable>
<Window.Content scrollable>{body}</Window.Content>
</Window>
);
};
const powerStatusMap = {
2: {
color: 'good',
externalPowerText: 'External Power',
chargingText: 'Fully Charged',
},
1: {
color: 'average',
externalPowerText: 'Low External Power',
chargingText: 'Charging',
},
0: {
color: 'bad',
externalPowerText: 'No External Power',
chargingText: 'Not Charging',
},
};
const malfMap = {
1: {
icon: 'terminal',
content: 'Override Programming',
action: 'hack',
},
// 2: {
// icon: 'caret-square-down',
// content: 'Shunt Core Process',
// action: 'occupy',
// },
// 3: {
// icon: 'caret-square-left',
// content: 'Return to Main Core',
// action: 'deoccupy',
// },
// 4: {
// icon: 'caret-square-down',
// content: 'Shunt Core Process',
// action: 'occupy',
// },
};
const ApcContent = (props, context) => {
const { act, data } = useBackend(context);
const locked = data.locked && !data.siliconUser;
const normallyLocked = data.normallyLocked;
const externalPowerStatus =
powerStatusMap[data.externalPower] || powerStatusMap[0];
const chargingStatus =
powerStatusMap[data.chargingStatus] || powerStatusMap[0];
const channelArray = data.powerChannels || [];
// const malfStatus = malfMap[data.malfStatus] || null;
const adjustedCellChange = data.powerCellStatus / 100;
return (
<Fragment>
<InterfaceLockNoticeBox
deny={data.emagged}
denialMessage={
<Fragment>
<Box color="bad" fontSize="1.5rem">
Fault in ID authenticator.
</Box>
<Box color="bad">Please contact maintenance for service.</Box>
</Fragment>
}
/>
<Section title="Power Status">
<LabeledList>
<LabeledList.Item
label="Main Breaker"
color={externalPowerStatus.color}
buttons={
<Button
icon={data.isOperating ? 'power-off' : 'times'}
content={data.isOperating ? 'On' : 'Off'}
selected={data.isOperating && !locked}
color={data.isOperating ? '' : 'bad'}
disabled={locked}
onClick={() => act('breaker')}
/>
}>
[ {externalPowerStatus.externalPowerText} ]
</LabeledList.Item>
<LabeledList.Item label="Power Cell">
<ProgressBar color="good" value={adjustedCellChange} />
</LabeledList.Item>
<LabeledList.Item
label="Charge Mode"
color={chargingStatus.color}
buttons={
<Button
icon={data.chargeMode ? 'sync' : 'times'}
content={data.chargeMode ? 'Auto' : 'Off'}
selected={data.chargeMode}
disabled={locked}
onClick={() => act('charge')}
/>
}>
[ {chargingStatus.chargingText} ]
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Power Channels">
<LabeledList>
{channelArray.map((channel) => {
const { topicParams } = channel;
return (
<LabeledList.Item
key={channel.title}
label={channel.title}
buttons={
<Fragment>
<Box
inline
mx={2}
color={channel.status >= 2 ? 'good' : 'bad'}>
{channel.status >= 2 ? 'On' : 'Off'}
</Box>
<Button
icon="sync"
content="Auto"
selected={
!locked &&
(channel.status === 1 || channel.status === 3)
}
disabled={locked}
onClick={() => act('channel', topicParams.auto)}
/>
<Button
icon="power-off"
content="On"
selected={!locked && channel.status === 2}
disabled={locked}
onClick={() => act('channel', topicParams.on)}
/>
<Button
icon="times"
content="Off"
selected={!locked && channel.status === 0}
disabled={locked}
onClick={() => act('channel', topicParams.off)}
/>
</Fragment>
}>
{channel.powerLoad} W
</LabeledList.Item>
);
})}
<LabeledList.Item label="Total Load">
{data.totalCharging ? (
<b>
{data.totalLoad} W (+ {data.totalCharging} W charging)
</b>
) : (
<b>{data.totalLoad} W</b>
)}
</LabeledList.Item>
</LabeledList>
</Section>
<Section
title="Misc"
buttons={
!!data.siliconUser && (
<Button
icon="lightbulb-o"
content="Overload"
onClick={() => act('overload')}
/>
)
}>
<LabeledList>
<LabeledList.Item
label="Cover Lock"
buttons={
<Button
icon={data.coverLocked ? 'lock' : 'unlock'}
content={data.coverLocked ? 'Engaged' : 'Disengaged'}
selected={data.coverLocked}
disabled={locked}
onClick={() => act('cover')}
/>
}
/>
<LabeledList.Item
label="Night Shift Lighting"
buttons={
<Fragment>
<Button
icon="lightbulb-o"
content="Disabled"
selected={data.nightshiftSetting === 2}
onClick={() =>
act('nightshift', {
nightshift: 2,
})
}
/>
<Button
icon="lightbulb-o"
content="Automatic"
selected={data.nightshiftSetting === 1}
onClick={() =>
act('nightshift', {
nightshift: 1,
})
}
/>
<Button
icon="lightbulb-o"
content="Enabled"
selected={data.nightshiftSetting === 3}
onClick={() =>
act('nightshift', {
nightshift: 3,
})
}
/>
</Fragment>
}
/>
<LabeledList.Item
label="Emergency Lighting"
buttons={
<Button
icon="lightbulb-o"
content={data.emergencyLights ? 'Enabled' : 'Disabled'}
selected={data.emergencyLights}
onClick={() => act('emergency_lighting')}
/>
}
/>
</LabeledList>
</Section>
</Fragment>
);
};
const GridCheck = (props, context) => {
return (
<FullscreenNotice title="System Failure">
<Box fontSize="1.5rem" bold>
<Icon
name="exclamation-triangle"
verticalAlign="middle"
size={3}
mr="1rem"
/>
</Box>
<Box fontSize="1.5rem" bold>
Power surge detected, grid check in effect...
</Box>
</FullscreenNotice>
);
};
const ApcFailure = (props, context) => {
const { data, act } = useBackend(context);
let rebootOptions = (
<Button
icon="repeat"
content="Restart Now"
color="good"
onClick={() => act('reboot')}
/>
);
if (data.locked && !data.siliconUser) {
rebootOptions = <Box color="bad">Swipe an ID card for manual reboot.</Box>;
}
return (
<Dimmer textAlign="center">
<Box color="bad">
<h1>SYSTEM FAILURE</h1>
</Box>
<Box color="average">
<h2>
I/O regulators malfunction detected! Waiting for system reboot...
</h2>
</Box>
<Box color="good">Automatic reboot in {data.failTime} seconds...</Box>
<Box mt={4}>{rebootOptions}</Box>
</Dimmer>
);
};

View File

@@ -1,220 +0,0 @@
import { useBackend, useSharedState } from '../backend';
import { Box, Button, LabeledList, Input, Section, Table, Tabs } from '../components';
import { Window } from '../layouts';
export const AccountsTerminal = (props, context) => {
const { act, data } = useBackend(context);
const { id_inserted, id_card, access_level, machine_id } = data;
return (
<Window width={400} height={640}>
<Window.Content scrollable>
<Section>
<LabeledList>
<LabeledList.Item label="Machine" color="average">
{machine_id}
</LabeledList.Item>
<LabeledList.Item label="ID">
<Button
icon={id_inserted ? 'eject' : 'sign-in-alt'}
fluid
content={id_card}
onClick={() => act('insert_card')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
{access_level > 0 && <AccountTerminalContent />}
</Window.Content>
</Window>
);
};
const AccountTerminalContent = (props, context) => {
const { act, data } = useBackend(context);
const { creating_new_account, detailed_account_view } = data;
return (
<Section title="Menu">
<Tabs>
<Tabs.Tab
selected={!creating_new_account && !detailed_account_view}
icon="home"
onClick={() => act('view_accounts_list')}>
Home
</Tabs.Tab>
<Tabs.Tab
selected={creating_new_account}
icon="cog"
onClick={() => act('create_account')}>
New Account
</Tabs.Tab>
<Tabs.Tab
disabled={creating_new_account}
icon="print"
onClick={() => act('print')}>
Print
</Tabs.Tab>
</Tabs>
{(creating_new_account && <NewAccountView />) ||
(detailed_account_view && <DetailedView />) || <ListView />}
</Section>
);
};
const NewAccountView = (props, context) => {
const { act } = useBackend(context);
const [holder, setHolder] = useSharedState(context, 'holder', '');
const [newMoney, setMoney] = useSharedState(context, 'money', '');
return (
<Section title="Create Account" level={2}>
<LabeledList>
<LabeledList.Item label="Account Holder">
<Input value={holder} fluid onInput={(e, val) => setHolder(val)} />
</LabeledList.Item>
<LabeledList.Item label="Initial Deposit">
<Input value={newMoney} fluid onInput={(e, val) => setMoney(val)} />
</LabeledList.Item>
</LabeledList>
<Button
disabled={!holder || !newMoney}
mt={1}
fluid
icon="plus"
onClick={() =>
act('finalise_create_account', {
holder_name: holder,
starting_funds: newMoney,
})
}
content="Create"
/>
</Section>
);
};
const DetailedView = (props, context) => {
const { act, data } = useBackend(context);
const {
access_level,
station_account_number,
account_number,
owner_name,
money,
suspended,
transactions,
} = data;
return (
<Section
title="Account Details"
level={2}
buttons={
<Button
icon="ban"
selected={suspended}
content="Suspend"
onClick={() => act('toggle_suspension')}
/>
}>
<LabeledList>
<LabeledList.Item label="Account Number">
#{account_number}
</LabeledList.Item>
<LabeledList.Item label="Holder">{owner_name}</LabeledList.Item>
<LabeledList.Item label="Balance">{money}</LabeledList.Item>
<LabeledList.Item label="Status" color={suspended ? 'bad' : 'good'}>
{suspended ? 'SUSPENDED' : 'Active'}
</LabeledList.Item>
</LabeledList>
<Section title="CentCom Administrator" level={2} mt={1}>
<LabeledList>
<LabeledList.Item label="Payroll">
<Button.Confirm
color="bad"
fluid
icon="ban"
confirmIcon="ban"
content="Revoke"
confirmContent="This cannot be undone."
disabled={account_number === station_account_number}
onClick={() => act('revoke_payroll')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
{access_level >= 2 && (
<Section title="Silent Funds Transfer" level={2}>
<Button
icon="plus"
onClick={() => act('add_funds')}
content="Add Funds"
/>
<Button
icon="plus"
onClick={() => act('remove_funds')}
content="Remove Funds"
/>
</Section>
)}
<Section title="Transactions" level={2} mt={1}>
<Table>
<Table.Row header>
<Table.Cell>Timestamp</Table.Cell>
<Table.Cell>Target</Table.Cell>
<Table.Cell>Reason</Table.Cell>
<Table.Cell>Value</Table.Cell>
<Table.Cell>Terminal</Table.Cell>
</Table.Row>
{transactions.map((trans, i) => (
<Table.Row key={i}>
<Table.Cell>
{trans.date} {trans.time}
</Table.Cell>
<Table.Cell>{trans.target_name}</Table.Cell>
<Table.Cell>{trans.purpose}</Table.Cell>
<Table.Cell>{trans.amount}</Table.Cell>
<Table.Cell>{trans.source_terminal}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
</Section>
);
};
const ListView = (props, context) => {
const { act, data } = useBackend(context);
const { accounts } = data;
return (
<Section title="NanoTrasen Accounts" level={2}>
{(accounts.length && (
<LabeledList>
{accounts.map((acc) => (
<LabeledList.Item
label={acc.owner_name + acc.suspended}
color={acc.suspended ? 'bad' : null}
key={acc.account_index}>
<Button
fluid
content={'#' + acc.account_number}
onClick={() =>
act('view_account_detail', {
'account_index': acc.account_index,
})
}
/>
</LabeledList.Item>
))}
</LabeledList>
)) || <Box color="bad">There are no accounts available.</Box>}
</Section>
);
};

View File

@@ -1,104 +0,0 @@
import { sortBy } from 'common/collections';
import { useBackend } from '../backend';
import { Button, Section, Table } from '../components';
import { Window } from '../layouts';
type Data = {
shuttles: Shuttle[];
overmap_ships: OvermapShip[];
};
type Shuttle = {
ref: string;
name: string;
current_location;
status;
};
type OvermapShip = {
ref: string;
name: string;
};
export const AdminShuttleController = () => {
return (
<Window width={600} height={600} resizable>
<Window.Content scrollable>
<ShuttleList />
</Window.Content>
</Window>
);
};
export const ShuttleList = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { shuttles, overmap_ships } = data;
return (
<Section noTopPadding>
<Section title="Classic Shuttles">
<Table>
{sortBy((f: Shuttle) => f.name)(shuttles).map((shuttle) => (
<Table.Row key={shuttle.ref}>
<Table.Cell collapsing>
<Button
m={0}
content="JMP"
onClick={() => act('adminobserve', { ref: shuttle.ref })}
/>
</Table.Cell>
<Table.Cell collapsing>
<Button
m={0}
content="Fly"
onClick={() => act('classicmove', { ref: shuttle.ref })}
/>
</Table.Cell>
<Table.Cell>{shuttle.name}</Table.Cell>
<Table.Cell>{shuttle.current_location}</Table.Cell>
<Table.Cell>{shuttleStatusToWords(shuttle.status)}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
<Section title="Overmap Ships">
<Table>
{sortBy((f: OvermapShip) => f.name?.toLowerCase() || f.name || f.ref)(
overmap_ships
).map((ship) => (
<Table.Row key={ship.ref}>
<Table.Cell collapsing>
<Button
content="JMP"
onClick={() => act('adminobserve', { ref: ship.ref })}
/>
</Table.Cell>
<Table.Cell collapsing>
<Button
content="Control"
onClick={() => act('overmap_control', { ref: ship.ref })}
/>
</Table.Cell>
<Table.Cell>{ship.name}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
</Section>
);
};
/* Helpers */
const shuttleStatusToWords = (status) => {
switch (status) {
case 0:
return 'Idle';
case 1:
return 'Warmup';
case 2:
return 'Transit';
default:
return 'UNK';
}
};

View File

@@ -1,49 +0,0 @@
import { BooleanLike } from 'common/react';
import { useBackend } from '../backend';
import { Button, Section, Table } from '../components';
import { Window } from '../layouts';
type Data = {
entries: { name: string; value: string }[];
electronic_warfare: BooleanLike;
};
export const AgentCard = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { entries, electronic_warfare } = data;
return (
<Window width={550} height={400} theme="syndicate">
<Window.Content>
<Section title="Info">
<Table>
{entries.map((a) => (
<Table.Row key={a.name}>
<Table.Cell>
<Button
onClick={() => act(a.name.toLowerCase().replace(/ /g, ''))}
icon="cog"
/>
</Table.Cell>
<Table.Cell>{a.name}</Table.Cell>
<Table.Cell>{a.value}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
<Section title="Electronic Warfare">
<Button.Checkbox
checked={electronic_warfare}
content={
electronic_warfare
? 'Electronic warfare is enabled. This will prevent you from being tracked by the AI.'
: 'Electronic warfare disabled.'
}
onClick={() => act('electronic_warfare')}
/>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,196 +0,0 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
const dangerMap = {
2: {
color: 'good',
localStatusText: 'Optimal',
},
1: {
color: 'average',
localStatusText: 'Caution',
},
0: {
color: 'bad',
localStatusText: 'Offline',
},
};
export const AiAirlock = (props, context) => {
const { act, data } = useBackend(context);
const statusMain = dangerMap[data.power.main] || dangerMap[0];
const statusBackup = dangerMap[data.power.backup] || dangerMap[0];
const statusElectrify = dangerMap[data.shock] || dangerMap[0];
return (
<Window width={500} height={390}>
<Window.Content>
<Section title="Power Status">
<LabeledList>
<LabeledList.Item
label="Main"
color={statusMain.color}
buttons={
<Button
icon="lightbulb-o"
disabled={!data.power.main}
content="Disrupt"
onClick={() => act('disrupt-main')}
/>
}>
{data.power.main ? 'Online' : 'Offline'}{' '}
{((!data.wires.main_1 || !data.wires.main_2) &&
'[Wires have been cut!]') ||
(data.power.main_timeleft > 0 &&
`[${data.power.main_timeleft}s]`)}
</LabeledList.Item>
<LabeledList.Item
label="Backup"
color={statusBackup.color}
buttons={
<Button
icon="lightbulb-o"
disabled={!data.power.backup}
content="Disrupt"
onClick={() => act('disrupt-backup')}
/>
}>
{data.power.backup ? 'Online' : 'Offline'}{' '}
{((!data.wires.backup_1 || !data.wires.backup_2) &&
'[Wires have been cut!]') ||
(data.power.backup_timeleft > 0 &&
`[${data.power.backup_timeleft}s]`)}
</LabeledList.Item>
<LabeledList.Item
label="Electrify"
color={statusElectrify.color}
buttons={
<Fragment>
<Button
icon="wrench"
disabled={!(data.wires.shock && data.shock === 0)}
content="Restore"
onClick={() => act('shock-restore')}
/>
<Button
icon="bolt"
disabled={!data.wires.shock}
content="Temporary"
onClick={() => act('shock-temp')}
/>
<Button
icon="bolt"
disabled={!data.wires.shock}
content="Permanent"
onClick={() => act('shock-perm')}
/>
</Fragment>
}>
{data.shock === 2 ? 'Safe' : 'Electrified'}{' '}
{(!data.wires.shock && '[Wires have been cut!]') ||
(data.shock_timeleft > 0 && `[${data.shock_timeleft}s]`) ||
(data.shock_timeleft === -1 && '[Permanent]')}
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Access and Door Control">
<LabeledList>
<LabeledList.Item
label="ID Scan"
color="bad"
buttons={
<Button
icon={data.id_scanner ? 'power-off' : 'times'}
content={data.id_scanner ? 'Enabled' : 'Disabled'}
selected={data.id_scanner}
disabled={!data.wires.id_scanner}
onClick={() => act('idscan-toggle')}
/>
}>
{!data.wires.id_scanner && '[Wires have been cut!]'}
</LabeledList.Item>
<LabeledList.Divider />
<LabeledList.Item
label="Door Bolts"
color="bad"
buttons={
<Button
icon={data.locked ? 'lock' : 'unlock'}
content={data.locked ? 'Lowered' : 'Raised'}
selected={data.locked}
disabled={!data.wires.bolts}
onClick={() => act('bolt-toggle')}
/>
}>
{!data.wires.bolts && '[Wires have been cut!]'}
</LabeledList.Item>
<LabeledList.Item
label="Door Bolt Lights"
color="bad"
buttons={
<Button
icon={data.lights ? 'power-off' : 'times'}
content={data.lights ? 'Enabled' : 'Disabled'}
selected={data.lights}
disabled={!data.wires.lights}
onClick={() => act('light-toggle')}
/>
}>
{!data.wires.lights && '[Wires have been cut!]'}
</LabeledList.Item>
<LabeledList.Item
label="Door Force Sensors"
color="bad"
buttons={
<Button
icon={data.safe ? 'power-off' : 'times'}
content={data.safe ? 'Enabled' : 'Disabled'}
selected={data.safe}
disabled={!data.wires.safe}
onClick={() => act('safe-toggle')}
/>
}>
{!data.wires.safe && '[Wires have been cut!]'}
</LabeledList.Item>
<LabeledList.Item
label="Door Timing Safety"
color="bad"
buttons={
<Button
icon={data.speed ? 'power-off' : 'times'}
content={data.speed ? 'Enabled' : 'Disabled'}
selected={data.speed}
disabled={!data.wires.timing}
onClick={() => act('speed-toggle')}
/>
}>
{!data.wires.timing && '[Wires have been cut!]'}
</LabeledList.Item>
<LabeledList.Divider />
<LabeledList.Item
label="Door Control"
color="bad"
buttons={
<Button
icon={data.opened ? 'sign-out-alt' : 'sign-in-alt'}
content={data.opened ? 'Open' : 'Closed'}
selected={data.opened}
disabled={data.locked || data.welded}
onClick={() => act('open-close')}
/>
}>
{!!(data.locked || data.welded) && (
<span>
[Door is {data.locked ? 'bolted' : ''}
{data.locked && data.welded ? ' and ' : ''}
{data.welded ? 'welded' : ''}!]
</span>
)}
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,86 +0,0 @@
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { Box, Button, LabeledList, NoticeBox, ProgressBar, Section } from '../components';
import { Window } from '../layouts';
export const AiRestorer = () => {
return (
<Window width={370} height={360} resizable>
<Window.Content scrollable>
<AiRestorerContent />
</Window.Content>
</Window>
);
};
export const AiRestorerContent = (props, context) => {
const { act, data } = useBackend(context);
const {
AI_present,
error,
name,
laws,
isDead,
restoring,
health,
ejectable,
} = data;
return (
<Fragment>
{error && <NoticeBox textAlign="center">{error}</NoticeBox>}
{!!ejectable && (
<Button
fluid
icon="eject"
content={AI_present ? name : '----------'}
disabled={!AI_present}
onClick={() => act('PRG_eject')}
/>
)}
{!!AI_present && (
<Section
title={ejectable ? 'System Status' : name}
buttons={
<Box inline bold color={isDead ? 'bad' : 'good'}>
{isDead ? 'Nonfunctional' : 'Functional'}
</Box>
}>
<LabeledList>
<LabeledList.Item label="Integrity">
<ProgressBar
value={health}
minValue={0}
maxValue={100}
ranges={{
good: [70, Infinity],
average: [50, 70],
bad: [-Infinity, 50],
}}
/>
</LabeledList.Item>
</LabeledList>
{!!restoring && (
<Box bold textAlign="center" fontSize="20px" color="good" mt={1}>
RECONSTRUCTION IN PROGRESS
</Box>
)}
<Button
fluid
icon="plus"
content="Begin Reconstruction"
disabled={restoring}
mt={1}
onClick={() => act('PRG_beginReconstruction')}
/>
<Section title="Laws" level={2}>
{laws.map((law) => (
<Box key={law} className="candystripe">
{law}
</Box>
))}
</Section>
</Section>
)}
</Fragment>
);
};

View File

@@ -1,77 +0,0 @@
import { useBackend } from '../backend';
import { Box, Icon, LabeledList, ProgressBar, Section } from '../components';
import { Window } from '../layouts';
import { FullscreenNotice } from './common/FullscreenNotice';
export const AiSupermatter = (props, context) => {
const { data } = useBackend(context);
const { integrity_percentage, ambient_temp, ambient_pressure, detonating } =
data;
let body = <AiSupermatterContent />;
if (detonating) {
body = <AiSupermatterDetonation />;
}
return (
<Window width={500} height={300}>
<Window.Content>{body}</Window.Content>
</Window>
);
};
const AiSupermatterDetonation = (props, context) => (
<FullscreenNotice title="DETONATION IMMINENT">
<Box fontSize="1.5rem" bold color="bad">
<Icon
color="bad"
name="exclamation-triangle"
verticalAlign="middle"
size={3}
mr="1rem"
/>
<Box color="bad">CRYSTAL DELAMINATING</Box>
<Box color="bad">Evacuate area immediately</Box>
</Box>
</FullscreenNotice>
);
const AiSupermatterContent = (props, context) => {
const { data } = useBackend(context);
const { integrity_percentage, ambient_temp, ambient_pressure } = data;
return (
<Section title="Status">
<LabeledList>
<LabeledList.Item label="Crystal Integrity">
<ProgressBar
value={integrity_percentage}
maxValue={100}
ranges={{
good: [90, Infinity],
average: [25, 90],
bad: [-Infinity, 25],
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Environment Temperature">
<ProgressBar
value={ambient_temp}
maxValue={10000}
ranges={{
bad: [5000, Infinity],
average: [4000, 5000],
good: [-Infinity, 4000],
}}>
{ambient_temp} K
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Environment Pressure">
{ambient_pressure} kPa
</LabeledList.Item>
</LabeledList>
</Section>
);
};

View File

@@ -1,312 +0,0 @@
import { toFixed } from 'common/math';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, LabeledList, Section } from '../components';
import { getGasLabel, getGasColor } from '../constants';
import { Window } from '../layouts';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
import { Vent, Scrubber } from './common/AtmosControls';
export const AirAlarm = (props, context) => {
const { act, data } = useBackend(context);
const locked = data.locked && !data.siliconUser && !data.remoteUser;
return (
<Window width={440} height={650} resizable>
<Window.Content scrollable>
<InterfaceLockNoticeBox />
<AirAlarmStatus />
<AirAlarmUnlockedControl />
{!locked && <AirAlarmControl />}
</Window.Content>
</Window>
);
};
const AirAlarmStatus = (props, context) => {
const { data } = useBackend(context);
const entries = (data.environment_data || []).filter(
(entry) => entry.value >= 0.01
);
const dangerMap = {
0: {
color: 'good',
localStatusText: 'Optimal',
},
1: {
color: 'average',
localStatusText: 'Caution',
},
2: {
color: 'bad',
localStatusText: 'Danger (Internals Required)',
},
};
const localStatus = dangerMap[data.danger_level] || dangerMap[0];
return (
<Section title="Air Status">
<LabeledList>
{(entries.length > 0 && (
<Fragment>
{entries.map((entry) => {
const status = dangerMap[entry.danger_level] || dangerMap[0];
return (
<LabeledList.Item
key={entry.name}
label={getGasLabel(entry.name)}
color={status.color}>
{toFixed(entry.value, 2)}
{entry.unit}
</LabeledList.Item>
);
})}
<LabeledList.Item label="Local status" color={localStatus.color}>
{localStatus.localStatusText}
</LabeledList.Item>
<LabeledList.Item
label="Area status"
color={data.atmos_alarm || data.fire_alarm ? 'bad' : 'good'}>
{(data.atmos_alarm && 'Atmosphere Alarm') ||
(data.fire_alarm && 'Fire Alarm') ||
'Nominal'}
</LabeledList.Item>
</Fragment>
)) || (
<LabeledList.Item label="Warning" color="bad">
Cannot obtain air sample for analysis.
</LabeledList.Item>
)}
{!!data.emagged && (
<LabeledList.Item label="Warning" color="bad">
Safety measures offline. Device may exhibit abnormal behavior.
</LabeledList.Item>
)}
</LabeledList>
</Section>
);
};
const AirAlarmUnlockedControl = (props, context) => {
const { act, data } = useBackend(context);
const { target_temperature, rcon } = data;
return (
<Section title="Comfort Settings">
<LabeledList>
<LabeledList.Item label="Remote Control">
<Button
selected={rcon === 1}
content="Off"
onClick={() => act('rcon', { 'rcon': 1 })}
/>
<Button
selected={rcon === 2}
content="Auto"
onClick={() => act('rcon', { 'rcon': 2 })}
/>
<Button
selected={rcon === 3}
content="On"
onClick={() => act('rcon', { 'rcon': 3 })}
/>
</LabeledList.Item>
<LabeledList.Item label="Thermostat">
<Button
content={target_temperature}
onClick={() => act('temperature')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
);
};
const AIR_ALARM_ROUTES = {
home: {
title: 'Air Controls',
component: () => AirAlarmControlHome,
},
vents: {
title: 'Vent Controls',
component: () => AirAlarmControlVents,
},
scrubbers: {
title: 'Scrubber Controls',
component: () => AirAlarmControlScrubbers,
},
modes: {
title: 'Operating Mode',
component: () => AirAlarmControlModes,
},
thresholds: {
title: 'Alarm Thresholds',
component: () => AirAlarmControlThresholds,
},
};
const AirAlarmControl = (props, context) => {
const [screen, setScreen] = useLocalState(context, 'screen');
const route = AIR_ALARM_ROUTES[screen] || AIR_ALARM_ROUTES.home;
const Component = route.component();
return (
<Section
title={route.title}
buttons={
screen && (
<Button
icon="arrow-left"
content="Back"
onClick={() => setScreen()}
/>
)
}>
<Component />
</Section>
);
};
// Home screen
// --------------------------------------------------------
const AirAlarmControlHome = (props, context) => {
const { act, data } = useBackend(context);
const [screen, setScreen] = useLocalState(context, 'screen');
const { mode, atmos_alarm } = data;
return (
<Fragment>
<Button
icon={atmos_alarm ? 'exclamation-triangle' : 'exclamation'}
color={atmos_alarm && 'caution'}
content="Area Atmosphere Alarm"
onClick={() => act(atmos_alarm ? 'reset' : 'alarm')}
/>
<Box mt={1} />
<Button
icon={mode === 3 ? 'exclamation-triangle' : 'exclamation'}
color={mode === 3 && 'danger'}
content="Panic Siphon"
onClick={() =>
act('mode', {
mode: mode === 3 ? 1 : 3,
})
}
/>
<Box mt={2} />
<Button
icon="sign-out-alt"
content="Vent Controls"
onClick={() => setScreen('vents')}
/>
<Box mt={1} />
<Button
icon="filter"
content="Scrubber Controls"
onClick={() => setScreen('scrubbers')}
/>
<Box mt={1} />
<Button
icon="cog"
content="Operating Mode"
onClick={() => setScreen('modes')}
/>
<Box mt={1} />
<Button
icon="chart-bar"
content="Alarm Thresholds"
onClick={() => setScreen('thresholds')}
/>
</Fragment>
);
};
// Vents
// --------------------------------------------------------
const AirAlarmControlVents = (props, context) => {
const { data } = useBackend(context);
const { vents } = data;
if (!vents || vents.length === 0) {
return 'Nothing to show';
}
return vents.map((vent) => <Vent key={vent.id_tag} vent={vent} />);
};
// Scrubbers
// --------------------------------------------------------
const AirAlarmControlScrubbers = (props, context) => {
const { data } = useBackend(context);
const { scrubbers } = data;
if (!scrubbers || scrubbers.length === 0) {
return 'Nothing to show';
}
return scrubbers.map((scrubber) => (
<Scrubber key={scrubber.id_tag} scrubber={scrubber} />
));
};
// Modes
// --------------------------------------------------------
const AirAlarmControlModes = (props, context) => {
const { act, data } = useBackend(context);
const { modes } = data;
if (!modes || modes.length === 0) {
return 'Nothing to show';
}
return modes.map((mode) => (
<Fragment key={mode.mode}>
<Button
icon={mode.selected ? 'check-square-o' : 'square-o'}
selected={mode.selected}
color={mode.selected && mode.danger && 'danger'}
content={mode.name}
onClick={() => act('mode', { mode: mode.mode })}
/>
<Box mt={1} />
</Fragment>
));
};
// Thresholds
// --------------------------------------------------------
const AirAlarmControlThresholds = (props, context) => {
const { act, data } = useBackend(context);
const { thresholds } = data;
return (
<table className="LabeledList" style={{ width: '100%' }}>
<thead>
<tr>
<td />
<td className="color-bad">min2</td>
<td className="color-average">min1</td>
<td className="color-average">max1</td>
<td className="color-bad">max2</td>
</tr>
</thead>
<tbody>
{thresholds.map((threshold) => (
<tr key={threshold.name}>
<td className="LabeledList__label">
<span className={'color-' + getGasColor(threshold.name)}>
{getGasLabel(threshold.name)}
</span>
</td>
{threshold.settings.map((setting) => (
<td key={setting.val}>
<Button
content={toFixed(setting.selected, 2)}
onClick={() =>
act('threshold', {
env: setting.env,
var: setting.val,
})
}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
);
};

View File

@@ -1,151 +0,0 @@
import { Loader } from './common/Loader';
import { useBackend, useLocalState } from '../backend';
import { KEY_ENTER, KEY_ESCAPE, KEY_LEFT, KEY_RIGHT, KEY_SPACE, KEY_TAB } from '../../common/keycodes';
import { Autofocus, Box, Button, Flex, Section, Stack } from '../components';
import { Window } from '../layouts';
type AlertModalData = {
autofocus: boolean;
buttons: string[];
large_buttons: boolean;
message: string;
swapped_buttons: boolean;
timeout: number;
title: string;
};
const KEY_DECREMENT = -1;
const KEY_INCREMENT = 1;
export const AlertModal = (props, context) => {
const { act, data } = useBackend<AlertModalData>(context);
const {
autofocus,
buttons = [],
large_buttons,
message = '',
timeout,
title,
} = data;
const [selected, setSelected] = useLocalState<number>(context, 'selected', 0);
// Dynamically sets window dimensions
const windowHeight =
115 +
(message.length > 30 ? Math.ceil(message.length / 4) : 0) +
(message.length && large_buttons ? 5 : 0);
const windowWidth = 325 + (buttons.length > 2 ? 55 : 0);
const onKey = (direction: number) => {
if (selected === 0 && direction === KEY_DECREMENT) {
setSelected(buttons.length - 1);
} else if (selected === buttons.length - 1 && direction === KEY_INCREMENT) {
setSelected(0);
} else {
setSelected(selected + direction);
}
};
return (
<Window height={windowHeight} title={title} width={windowWidth}>
{!!timeout && <Loader value={timeout} />}
<Window.Content
onKeyDown={(e) => {
const keyCode = window.event ? e.which : e.keyCode;
/**
* Simulate a click when pressing space or enter,
* allow keyboard navigation, override tab behavior
*/
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
act('choose', { choice: buttons[selected] });
} else if (keyCode === KEY_ESCAPE) {
act('cancel');
} else if (keyCode === KEY_LEFT) {
e.preventDefault();
onKey(KEY_DECREMENT);
} else if (keyCode === KEY_TAB || keyCode === KEY_RIGHT) {
e.preventDefault();
onKey(KEY_INCREMENT);
}
}}>
<Section fill>
<Stack fill vertical>
<Stack.Item grow m={1}>
<Box color="label" overflow="hidden">
{message}
</Box>
</Stack.Item>
<Stack.Item>
{!!autofocus && <Autofocus />}
<ButtonDisplay selected={selected} />
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
};
/**
* Displays a list of buttons ordered by user prefs.
* Technically this handles more than 2 buttons, but you
* should just be using a list input in that case.
*/
const ButtonDisplay = (props, context) => {
const { data } = useBackend<AlertModalData>(context);
const { buttons = [], large_buttons, swapped_buttons } = data;
const { selected } = props;
return (
<Flex
align="center"
direction={!swapped_buttons ? 'row-reverse' : 'row'}
fill
justify="space-around"
wrap>
{buttons?.map((button, index) =>
!!large_buttons && buttons.length < 3 ? (
<Flex.Item grow key={index}>
<AlertButton
button={button}
id={index.toString()}
selected={selected === index}
/>
</Flex.Item>
) : (
<Flex.Item key={index}>
<AlertButton
button={button}
id={index.toString()}
selected={selected === index}
/>
</Flex.Item>
)
)}
</Flex>
);
};
/**
* Displays a button with variable sizing.
*/
const AlertButton = (props, context) => {
const { act, data } = useBackend<AlertModalData>(context);
const { large_buttons } = data;
const { button, selected } = props;
const buttonWidth = button.length > 7 ? button.length : 7;
return (
<Button
fluid={!!large_buttons}
height={!!large_buttons && 2}
onClick={() => act('choose', { choice: button })}
m={0.5}
pl={2}
pr={2}
pt={large_buttons ? 0.33 : 0}
selected={selected}
textAlign="center"
width={!large_buttons && buttonWidth}>
{!large_buttons ? button : button.toUpperCase()}
</Button>
);
};

View File

@@ -1,110 +0,0 @@
import { useBackend } from '../backend';
import { Box, Button, NoticeBox, LabeledList, ProgressBar, Section, Table } from '../components';
import { Window } from '../layouts';
import { capitalize } from 'common/string';
export const AlgaeFarm = (props, context) => {
const { act, data } = useBackend(context);
const {
usePower,
materials,
last_flow_rate,
last_power_draw,
inputDir,
outputDir,
input,
output,
errorText,
} = data;
return (
<Window width={500} height={300} resizable>
<Window.Content>
{errorText && (
<NoticeBox warning>
<Box display="inline-block" verticalAlign="middle">
{errorText}
</Box>
</NoticeBox>
)}
<Section
title="Status"
buttons={
<Button
icon="power-off"
content="Processing"
selected={usePower === 2}
onClick={() => act('toggle')}
/>
}>
<LabeledList>
<LabeledList.Item label="Flow Rate">
{last_flow_rate} L/s
</LabeledList.Item>
<LabeledList.Item label="Power Draw">
{last_power_draw} W
</LabeledList.Item>
<LabeledList.Divider size={1} />
{materials.map((material) => (
<LabeledList.Item
key={material.name}
label={capitalize(material.display)}>
<ProgressBar
width="80%"
value={material.qty}
maxValue={material.max}>
{material.qty}/{material.max}
</ProgressBar>
<Button
ml={1}
content="Eject"
onClick={() =>
act('ejectMaterial', {
mat: material.name,
})
}
/>
</LabeledList.Item>
))}
</LabeledList>
<Table mt={1}>
<Table.Row>
<Table.Cell>
<Section title={'Gas Input (' + inputDir + ')'}>
{input ? (
<LabeledList>
<LabeledList.Item label="Total Pressure">
{input.pressure} kPa
</LabeledList.Item>
<LabeledList.Item label={input.name}>
{input.percent}% ({input.moles} moles)
</LabeledList.Item>
</LabeledList>
) : (
<Box color="bad">No connection detected.</Box>
)}
</Section>
</Table.Cell>
<Table.Cell>
<Section title={'Gas Output (' + outputDir + ')'}>
{output ? (
<LabeledList>
<LabeledList.Item label="Total Pressure">
{output.pressure} kPa
</LabeledList.Item>
<LabeledList.Item label={output.name}>
{output.percent}% ({output.moles} moles)
</LabeledList.Item>
</LabeledList>
) : (
<Box color="bad">No connection detected.</Box>
)}
</Section>
</Table.Cell>
</Table.Row>
</Table>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,509 +0,0 @@
import { sortBy } from 'common/collections';
import { capitalize, decodeHtmlEntities } from 'common/string';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, ByondUi, Flex, LabeledList, Section, Tabs, ColorBox } from '../components';
import { Window } from '../layouts';
export const AppearanceChanger = (props, context) => {
const { act, config, data } = useBackend(context);
const {
name,
specimen,
gender,
gender_id,
hair_style,
facial_hair_style,
ear_style,
tail_style,
wing_style,
markings,
change_race,
change_gender,
change_eye_color,
change_skin_tone,
change_skin_color,
change_hair_color,
change_facial_hair_color,
change_hair,
change_facial_hair,
mapRef,
} = data;
const { title } = config;
const change_color =
change_eye_color ||
change_skin_tone ||
change_skin_color ||
change_hair_color ||
change_facial_hair_color;
let firstAccesibleTab = -1;
if (change_race) {
firstAccesibleTab = 0;
} else if (change_gender) {
firstAccesibleTab = 1;
} else if (change_color) {
firstAccesibleTab = 2;
} else if (change_hair) {
firstAccesibleTab = 4;
} else if (change_facial_hair) {
firstAccesibleTab = 5;
}
const [tabIndex, setTabIndex] = useLocalState(
context,
'tabIndex',
firstAccesibleTab
);
return (
<Window width={700} height={650} title={decodeHtmlEntities(title)}>
<Window.Content>
<Section title="Reflection">
<Flex>
<Flex.Item grow={1}>
<LabeledList>
<LabeledList.Item label="Name">{name}</LabeledList.Item>
<LabeledList.Item
label="Species"
color={!change_race ? 'grey' : null}>
{specimen}
</LabeledList.Item>
<LabeledList.Item
label="Biological Sex"
color={!change_gender ? 'grey' : null}>
{gender ? capitalize(gender) : 'Not Set'}
</LabeledList.Item>
<LabeledList.Item
label="Gender Identity"
color={!change_color ? 'grey' : null}>
{gender_id ? capitalize(gender_id) : 'Not Set'}
</LabeledList.Item>
<LabeledList.Item
label="Hair Style"
color={!change_hair ? 'grey' : null}>
{hair_style ? capitalize(hair_style) : 'Not Set'}
</LabeledList.Item>
<LabeledList.Item
label="Facial Hair Style"
color={!change_facial_hair ? 'grey' : null}>
{facial_hair_style
? capitalize(facial_hair_style)
: 'Not Set'}
</LabeledList.Item>
<LabeledList.Item
label="Ear Style"
color={!change_hair ? 'grey' : null}>
{ear_style ? capitalize(ear_style) : 'Not Set'}
</LabeledList.Item>
<LabeledList.Item
label="Tail Style"
color={!change_hair ? 'grey' : null}>
{tail_style ? capitalize(tail_style) : 'Not Set'}
</LabeledList.Item>
<LabeledList.Item
label="Wing Style"
color={!change_hair ? 'grey' : null}>
{wing_style ? capitalize(wing_style) : 'Not Set'}
</LabeledList.Item>
</LabeledList>
</Flex.Item>
<Flex.Item>
<ByondUi
style={{
width: '256px',
height: '256px',
}}
params={{
id: mapRef,
type: 'map',
}}
/>
</Flex.Item>
</Flex>
</Section>
<Tabs>
{change_race ? (
<Tabs.Tab selected={tabIndex === 0} onClick={() => setTabIndex(0)}>
Race
</Tabs.Tab>
) : null}
{change_gender ? (
<Tabs.Tab selected={tabIndex === 1} onClick={() => setTabIndex(1)}>
Gender & Sex
</Tabs.Tab>
) : null}
{change_color ? (
<Tabs.Tab selected={tabIndex === 2} onClick={() => setTabIndex(2)}>
Colors
</Tabs.Tab>
) : null}
{change_hair ? (
<Fragment>
<Tabs.Tab
selected={tabIndex === 3}
onClick={() => setTabIndex(3)}>
Hair
</Tabs.Tab>
<Tabs.Tab
selected={tabIndex === 5}
onClick={() => setTabIndex(5)}>
Ear
</Tabs.Tab>
<Tabs.Tab
selected={tabIndex === 6}
onClick={() => setTabIndex(6)}>
Tail
</Tabs.Tab>
<Tabs.Tab
selected={tabIndex === 7}
onClick={() => setTabIndex(7)}>
Wing
</Tabs.Tab>
<Tabs.Tab
selected={tabIndex === 8}
onClick={() => setTabIndex(8)}>
Markings
</Tabs.Tab>
</Fragment>
) : null}
{change_facial_hair ? (
<Tabs.Tab selected={tabIndex === 4} onClick={() => setTabIndex(4)}>
Facial Hair
</Tabs.Tab>
) : null}
</Tabs>
<Box height="43%">
{change_race && tabIndex === 0 ? <AppearanceChangerSpecies /> : null}
{change_gender && tabIndex === 1 ? <AppearanceChangerGender /> : null}
{change_color && tabIndex === 2 ? <AppearanceChangerColors /> : null}
{change_hair && tabIndex === 3 ? <AppearanceChangerHair /> : null}
{change_facial_hair && tabIndex === 4 ? (
<AppearanceChangerFacialHair />
) : null}
{change_hair && tabIndex === 5 ? <AppearanceChangerEars /> : null}
{change_hair && tabIndex === 6 ? <AppearanceChangerTails /> : null}
{change_hair && tabIndex === 7 ? <AppearanceChangerWings /> : null}
{change_hair && tabIndex === 8 ? <AppearanceChangerMarkings /> : null}
</Box>
</Window.Content>
</Window>
);
};
const AppearanceChangerSpecies = (props, context) => {
const { act, data } = useBackend(context);
const { species, specimen } = data;
const sortedSpecies = sortBy((val) => val.specimen)(species || []);
return (
<Section title="Species" fill scrollable>
{sortedSpecies.map((spec) => (
<Button
key={spec.specimen}
content={spec.specimen}
selected={specimen === spec.specimen}
onClick={() => act('race', { race: spec.specimen })}
/>
))}
</Section>
);
};
const AppearanceChangerGender = (props, context) => {
const { act, data } = useBackend(context);
const { gender, gender_id, genders, id_genders } = data;
return (
<Section title="Gender & Sex" fill scrollable>
<LabeledList>
<LabeledList.Item label="Biological Sex">
{genders.map((g) => (
<Button
key={g.gender_key}
selected={g.gender_key === gender}
content={g.gender_name}
onClick={() => act('gender', { 'gender': g.gender_key })}
/>
))}
</LabeledList.Item>
<LabeledList.Item label="Gender Identity">
{id_genders.map((g) => (
<Button
key={g.gender_key}
selected={g.gender_key === gender_id}
content={g.gender_name}
onClick={() => act('gender_id', { 'gender_id': g.gender_key })}
/>
))}
</LabeledList.Item>
</LabeledList>
</Section>
);
};
const AppearanceChangerColors = (props, context) => {
const { act, data } = useBackend(context);
const {
change_eye_color,
change_skin_tone,
change_skin_color,
change_hair_color,
change_facial_hair_color,
eye_color,
skin_color,
hair_color,
facial_hair_color,
ears_color,
ears2_color,
tail_color,
tail2_color,
wing_color,
wing2_color,
} = data;
return (
<Section title="Colors" fill scrollable>
{change_eye_color ? (
<Box>
<ColorBox color={eye_color} mr={1} />
<Button content="Change Eye Color" onClick={() => act('eye_color')} />
</Box>
) : null}
{change_skin_tone ? (
<Box>
<Button content="Change Skin Tone" onClick={() => act('skin_tone')} />
</Box>
) : null}
{change_skin_color ? (
<Box>
<ColorBox color={skin_color} mr={1} />
<Button
content="Change Skin Color"
onClick={() => act('skin_color')}
/>
</Box>
) : null}
{change_hair_color ? (
<Fragment>
<Box>
<ColorBox color={hair_color} mr={1} />
<Button
content="Change Hair Color"
onClick={() => act('hair_color')}
/>
</Box>
<Box>
<ColorBox color={ears_color} mr={1} />
<Button
content="Change Ears Color"
onClick={() => act('ears_color')}
/>
</Box>
<Box>
<ColorBox color={ears2_color} mr={1} />
<Button
content="Change Secondary Ears Color"
onClick={() => act('ears2_color')}
/>
</Box>
<Box>
<ColorBox color={tail_color} mr={1} />
<Button
content="Change Tail Color"
onClick={() => act('tail_color')}
/>
</Box>
<Box>
<ColorBox color={tail2_color} mr={1} />
<Button
content="Change Secondary Tail Color"
onClick={() => act('tail2_color')}
/>
</Box>
<Box>
<ColorBox color={wing_color} mr={1} />
<Button
content="Change Wing Color"
onClick={() => act('wing_color')}
/>
</Box>
<Box>
<ColorBox color={wing2_color} mr={1} />
<Button
content="Change Secondary Wing Color"
onClick={() => act('wing2_color')}
/>
</Box>
</Fragment>
) : null}
{change_facial_hair_color ? (
<Box>
<ColorBox color={facial_hair_color} mr={1} />
<Button
content="Change Facial Hair Color"
onClick={() => act('facial_hair_color')}
/>
</Box>
) : null}
</Section>
);
};
const AppearanceChangerHair = (props, context) => {
const { act, data } = useBackend(context);
const { hair_style, hair_styles } = data;
return (
<Section title="Hair" fill scrollable>
{hair_styles.map((hair) => (
<Button
key={hair.hairstyle}
onClick={() => act('hair', { hair: hair.hairstyle })}
selected={hair.hairstyle === hair_style}
content={hair.hairstyle}
/>
))}
</Section>
);
};
const AppearanceChangerFacialHair = (props, context) => {
const { act, data } = useBackend(context);
const { facial_hair_style, facial_hair_styles } = data;
return (
<Section title="Facial Hair" fill scrollable>
{facial_hair_styles.map((hair) => (
<Button
key={hair.facialhairstyle}
onClick={() =>
act('facial_hair', { facial_hair: hair.facialhairstyle })
}
selected={hair.facialhairstyle === facial_hair_style}
content={hair.facialhairstyle}
/>
))}
</Section>
);
};
const AppearanceChangerEars = (props, context) => {
const { act, data } = useBackend(context);
const { ear_style, ear_styles } = data;
return (
<Section title="Ears" fill scrollable>
<Button
onClick={() => act('ear', { clear: true })}
selected={ear_style === null}
content="-- Not Set --"
/>
{sortBy((e) => e.name.toLowerCase())(ear_styles).map((ear) => (
<Button
key={ear.instance}
onClick={() => act('ear', { ref: ear.instance })}
selected={ear.name === ear_style}
content={ear.name}
/>
))}
</Section>
);
};
const AppearanceChangerTails = (props, context) => {
const { act, data } = useBackend(context);
const { tail_style, tail_styles } = data;
return (
<Section title="Tails" fill scrollable>
<Button
onClick={() => act('tail', { clear: true })}
selected={tail_style === null}
content="-- Not Set --"
/>
{sortBy((e) => e.name.toLowerCase())(tail_styles).map((tail) => (
<Button
key={tail.instance}
onClick={() => act('tail', { ref: tail.instance })}
selected={tail.name === tail_style}
content={tail.name}
/>
))}
</Section>
);
};
const AppearanceChangerWings = (props, context) => {
const { act, data } = useBackend(context);
const { wing_style, wing_styles } = data;
return (
<Section title="Wings" fill scrollable>
<Button
onClick={() => act('wing', { clear: true })}
selected={wing_style === null}
content="-- Not Set --"
/>
{sortBy((e) => e.name.toLowerCase())(wing_styles).map((wing) => (
<Button
key={wing.instance}
onClick={() => act('wing', { ref: wing.instance })}
selected={wing.name === wing_style}
content={wing.name}
/>
))}
</Section>
);
};
const AppearanceChangerMarkings = (props, context) => {
const { act, data } = useBackend(context);
const { markings } = data;
return (
<Section title="Markings" fill scrollable>
<Box>
<Button
content="Add Marking"
onClick={() => act('marking', { todo: 1, name: 'na' })}
/>
</Box>
<LabeledList>
{markings.map((m) => (
<LabeledList.Item key={m.marking_name} label={m.marking_name}>
<ColorBox color={m.marking_color} mr={1} />
<Button
content="Change Color"
onClick={() => act('marking', { todo: 4, name: m.marking_name })}
/>
<Button
content="-"
onClick={() => act('marking', { todo: 0, name: m.marking_name })}
/>
<Button
content="Move down"
onClick={() => act('marking', { todo: 3, name: m.marking_name })}
/>
<Button
content="Move up"
onClick={() => act('marking', { todo: 2, name: m.marking_name })}
/>
</LabeledList.Item>
))}
</LabeledList>
</Section>
);
};

View File

@@ -1,125 +0,0 @@
import { useBackend } from '../backend';
import { Box, Button, Flex, LabeledList, ProgressBar, Section } from '../components';
import { Window } from '../layouts';
export const ArcadeBattle = (props, context) => {
const { act, data } = useBackend(context);
const {
name,
temp,
enemyAction,
enemyName,
playerHP,
playerMP,
enemyHP,
enemyMP,
gameOver,
} = data;
return (
<Window width={400} height={240} resizable>
<Window.Content>
<Section title={enemyName} textAlign="center">
<Section color="label">
<Box>{temp}</Box>
<Box>{!gameOver && enemyAction}</Box>
</Section>
<Flex spacing={1}>
<Flex.Item>
<LabeledList>
<LabeledList.Item label="Player Health">
<ProgressBar
value={playerHP}
minValue={0}
maxValue={30}
ranges={{
olive: [31, Infinity],
good: [20, 31],
average: [10, 20],
bad: [-Infinity, 10],
}}>
{playerHP}HP
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Player Magic">
<ProgressBar
value={playerMP}
minValue={0}
maxValue={10}
ranges={{
purple: [11, Infinity],
violet: [3, 11],
bad: [-Infinity, 3],
}}>
{playerMP}MP
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Flex.Item>
<Flex.Item>
<LabeledList>
<LabeledList.Item label="Enemy HP">
<ProgressBar
value={enemyHP}
minValue={0}
maxValue={45}
ranges={{
olive: [31, Infinity],
good: [20, 31],
average: [10, 20],
bad: [-Infinity, 10],
}}>
{enemyHP}HP
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Flex.Item>
</Flex>
{(gameOver && (
<Button
fluid
mt={1}
color="green"
content="New Game"
onClick={() => act('newgame')}
/>
)) || (
<Flex mt={2} justify="space-between" spacing={1}>
<Flex.Item grow={1}>
<Button
fluid
icon="fist-raised"
tooltip="Go in for the kill!"
tooltipPosition="top"
onClick={() => act('attack')}
content="Attack!"
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
fluid
icon="band-aid"
tooltip="Heal yourself!"
tooltipPosition="top"
onClick={() => act('heal')}
content="Heal!"
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
fluid
icon="magic"
tooltip="Recharge your magic!"
tooltipPosition="top"
onClick={() => act('charge')}
content="Recharge!"
/>
</Flex.Item>
</Flex>
)}
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,113 +0,0 @@
import { Flex, Button, Box, LabeledList, Section } from '../components';
import { useBackend, useLocalState } from '../backend';
import { Window } from '../layouts';
import { toTitleCase } from 'common/string';
export const AreaScrubberControl = (props, context) => {
const { act, data } = useBackend(context);
const [showArea, setShowArea] = useLocalState(context, 'showArea', false);
const { scrubbers } = data;
if (!scrubbers) {
return (
<Section title="Error">
<Box color="bad">No Scrubbers Detected.</Box>
<Button
fluid
icon="search"
content="Scan"
onClick={() => act('scan')}
/>
</Section>
);
}
return (
<Window width={600} height={400} resizable>
<Window.Content scrollable>
<Section>
<Flex wrap="wrap">
<Flex.Item m="2px" basis="49%">
<Button
textAlign="center"
fluid
icon="search"
content="Scan"
onClick={() => act('scan')}
/>
</Flex.Item>
<Flex.Item m="2px" basis="49%" grow={1}>
<Button
textAlign="center"
fluid
icon="layer-group"
content="Show Areas"
selected={showArea}
onClick={() => setShowArea(!showArea)}
/>
</Flex.Item>
<Flex.Item m="2px" basis="49%">
<Button
textAlign="center"
fluid
icon="toggle-on"
content="All On"
onClick={() => act('allon')}
/>
</Flex.Item>
<Flex.Item m="2px" basis="49%" grow={1}>
<Button
textAlign="center"
fluid
icon="toggle-off"
content="All Off"
onClick={() => act('alloff')}
/>
</Flex.Item>
</Flex>
<Flex wrap="wrap">
{scrubbers.map((scrubber) => (
<Flex.Item m="2px" key={scrubber.id} basis="32%">
<BigScrubber scrubber={scrubber} showArea={showArea} />
</Flex.Item>
))}
</Flex>
</Section>
</Window.Content>
</Window>
);
};
const BigScrubber = (props, context) => {
const { act } = useBackend(context);
const { scrubber, showArea } = props;
return (
<Section title={scrubber.name}>
<Button
fluid
icon="power-off"
content={scrubber.on ? 'Enabled' : 'Disabled'}
selected={scrubber.on}
onClick={() => act('toggle', { id: scrubber.id })}
/>
<LabeledList>
<LabeledList.Item label="Pressure">
{scrubber.pressure} kPa
</LabeledList.Item>
<LabeledList.Item label="Flow Rate">
{scrubber.flow_rate} L/s
</LabeledList.Item>
<LabeledList.Item label="Load">{scrubber.load} W</LabeledList.Item>
{showArea && (
<LabeledList.Item label="Area">
{toTitleCase(scrubber.area)}
</LabeledList.Item>
)}
</LabeledList>
</Section>
);
};

View File

@@ -1,42 +0,0 @@
import { BooleanLike } from 'common/react';
import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';
type Data = {
on: BooleanLike;
visible: BooleanLike;
};
export const AssemblyInfrared = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { on, visible } = data;
return (
<Window>
<Window.Content>
<Section title="Infrared Unit">
<LabeledList>
<LabeledList.Item label="Laser">
<Button
icon="power-off"
fluid
selected={on}
onClick={() => act('state')}>
{on ? 'On' : 'Off'}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Visibility">
<Button
icon="eye"
fluid
selected={visible}
onClick={() => act('visible')}>
{visible ? 'Able to be seen' : 'Invisible'}
</Button>
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,62 +0,0 @@
import { round } from 'common/math';
import { useBackend } from '../backend';
import { Button, LabeledList, NumberInput, Section } from '../components';
import { Window } from '../layouts';
import { formatTime } from '../format';
export const AssemblyProx = (props, context) => {
const { act, data } = useBackend(context);
const { timing, time, range, maxRange, scanning } = data;
return (
<Window>
<Window.Content>
<Section title="Timing Unit">
<LabeledList>
<LabeledList.Item
label="Timer"
buttons={
<Button
icon="stopwatch"
selected={timing}
onClick={() => act('timing')}>
{timing ? 'Counting Down' : 'Disabled'}
</Button>
}>
<NumberInput
animated
fluid
value={time / 10}
minValue={0}
maxValue={600}
format={(val) => formatTime(round(val))}
onDrag={(e, val) => act('set_time', { time: val })}
/>
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Prox Unit">
<LabeledList>
<LabeledList.Item label="Range">
<NumberInput
minValue={1}
value={range}
maxValue={maxRange}
onDrag={(e, val) => act('range', { range: val })}
/>
</LabeledList.Item>
<LabeledList.Item label="Armed">
<Button
mr={1}
icon={scanning ? 'lock' : 'lock-open'}
selected={scanning}
onClick={() => act('scanning')}>
{scanning ? 'ARMED' : 'Unarmed'}
</Button>
Movement sensor is active when armed!
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,40 +0,0 @@
import { round } from 'common/math';
import { useBackend } from '../backend';
import { Button, LabeledList, NumberInput, Section } from '../components';
import { Window } from '../layouts';
import { formatTime } from '../format';
export const AssemblyTimer = (props, context) => {
const { act, data } = useBackend(context);
const { timing, time } = data;
return (
<Window>
<Window.Content>
<Section title="Timing Unit">
<LabeledList>
<LabeledList.Item
label="Timer"
buttons={
<Button
icon="stopwatch"
selected={timing}
onClick={() => act('timing')}>
{timing ? 'Counting Down' : 'Disabled'}
</Button>
}>
<NumberInput
animated
fluid
value={time / 10}
minValue={0}
maxValue={600}
format={(val) => formatTime(round(val))}
onDrag={(e, val) => act('set_time', { time: val })}
/>
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,45 +0,0 @@
import { useBackend } from '../backend';
import { Button, Section } from '../components';
import { Window } from '../layouts';
export const AtmosAlertConsole = (props, context) => {
const { act, data } = useBackend(context);
const priorityAlerts = data.priority_alarms || [];
const minorAlerts = data.minor_alarms || [];
return (
<Window width={350} height={300} resizable>
<Window.Content scrollable>
<Section title="Alarms">
<ul>
{priorityAlerts.length === 0 && (
<li className="color-good">No Priority Alerts</li>
)}
{priorityAlerts.map((alert) => (
<li key={alert.name}>
<Button
icon="times"
content={alert.name}
color="bad"
onClick={() => act('clear', { ref: alert.ref })}
/>
</li>
))}
{minorAlerts.length === 0 && (
<li className="color-good">No Minor Alerts</li>
)}
{minorAlerts.map((alert) => (
<li key={alert.name}>
<Button
icon="times"
content={alert.name}
color="average"
onClick={() => act('clear', { ref: alert.ref })}
/>
</li>
))}
</ul>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,91 +0,0 @@
import { sortBy } from 'common/collections';
import { Window } from '../layouts';
import { Fragment } from 'inferno';
import { Button, Box, Tabs, Icon, Section, NanoMap } from '../components';
import { useBackend, useLocalState } from '../backend';
import { createLogger } from '../logging';
const logger = createLogger('fuck');
export const AtmosControl = (props, context) => {
return (
<Window width={600} height={440} resizable>
<Window.Content scrollable>
<AtmosControlContent />
</Window.Content>
</Window>
);
};
export const AtmosControlContent = (props, context) => {
const { act, data, config } = useBackend(context);
let sortedAlarms = sortBy((alarm) => alarm.name)(data.alarms || []);
// sortedAlarms = sortedAlarms.slice(1, 3);
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
const [zoom, setZoom] = useLocalState(context, 'zoom', 1);
let body;
// Alarms View
if (tabIndex === 0) {
body = (
<Section title="Alarms">
{sortedAlarms.map((alarm) => (
<Button
key={alarm.name}
content={alarm.name}
color={
alarm.danger === 2 ? 'bad' : alarm.danger === 1 ? 'average' : ''
}
onClick={() => act('alarm', { 'alarm': alarm.ref })}
/>
))}
</Section>
);
} else if (tabIndex === 1) {
// Please note, if you ever change the zoom values,
// you MUST update styles/components/Tooltip.scss
// and change the @for scss to match.
body = (
<Box height="526px" mb="0.5rem" overflow="hidden">
<NanoMap onZoom={(v) => setZoom(v)}>
{sortedAlarms
.filter((x) => ~~x.z === ~~config.mapZLevel)
.map((cm) => (
<NanoMap.Marker
key={cm.ref}
x={cm.x}
y={cm.y}
zoom={zoom}
icon="bell"
tooltip={cm.name}
color={cm.danger ? 'red' : 'green'}
onClick={() => act('alarm', { 'alarm': cm.ref })}
/>
))}
</NanoMap>
</Box>
);
}
return (
<Fragment>
<Tabs>
<Tabs.Tab
key="AlarmView"
selected={0 === tabIndex}
onClick={() => setTabIndex(0)}>
<Icon name="table" /> Alarm View
</Tabs.Tab>
<Tabs.Tab
key="MapView"
selected={1 === tabIndex}
onClick={() => setTabIndex(1)}>
<Icon name="map-marked-alt" /> Map View
</Tabs.Tab>
</Tabs>
<Box m={2}>{body}</Box>
</Fragment>
);
};

View File

@@ -1,72 +0,0 @@
import { useBackend } from '../backend';
import { Button, LabeledList, NumberInput, Section, AnimatedNumber, Box } from '../components';
import { Window } from '../layouts';
export const AtmosFilter = (props, context) => {
const { act, data } = useBackend(context);
const filterTypes = data.filter_types || [];
return (
<Window width={390} height={187} resizable>
<Window.Content>
<Section>
<LabeledList>
<LabeledList.Item label="Power">
<Button
icon={data.on ? 'power-off' : 'times'}
content={data.on ? 'On' : 'Off'}
selected={data.on}
onClick={() => act('power')}
/>
</LabeledList.Item>
<LabeledList.Item label="Transfer Rate">
<Box inline mr={1}>
<AnimatedNumber
value={data.last_flow_rate}
format={(val) => val + ' L/s'}
/>
</Box>
<NumberInput
animated
value={parseFloat(data.rate)}
width="63px"
unit="L/s"
minValue={0}
maxValue={200}
onDrag={(e, value) =>
act('rate', {
rate: value,
})
}
/>
<Button
ml={1}
icon="plus"
content="Max"
disabled={data.rate === data.max_rate}
onClick={() =>
act('rate', {
rate: 'max',
})
}
/>
</LabeledList.Item>
<LabeledList.Item label="Filter">
{filterTypes.map((filter) => (
<Button
key={filter.name}
selected={filter.selected}
content={filter.name}
onClick={() =>
act('filter', {
filterset: filter.f_type,
})
}
/>
))}
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,88 +0,0 @@
import { useBackend } from '../backend';
import { Button, LabeledList, NumberInput, Section } from '../components';
import { Window } from '../layouts';
export const AtmosMixer = (props, context) => {
const { act, data } = useBackend(context);
return (
<Window width={370} height={195} resizable>
<Window.Content>
<Section>
<LabeledList>
<LabeledList.Item label="Power">
<Button
icon={data.on ? 'power-off' : 'times'}
content={data.on ? 'On' : 'Off'}
selected={data.on}
onClick={() => act('power')}
/>
</LabeledList.Item>
<LabeledList.Item label="Output Pressure">
<NumberInput
animated
value={parseFloat(data.set_pressure)}
unit="kPa"
width="75px"
minValue={0}
maxValue={data.max_pressure}
step={10}
onChange={(e, value) =>
act('pressure', {
pressure: value,
})
}
/>
<Button
ml={1}
icon="plus"
content="Max"
disabled={data.set_pressure === data.max_pressure}
onClick={() =>
act('pressure', {
pressure: 'max',
})
}
/>
</LabeledList.Item>
<LabeledList.Divider size={1} />
<LabeledList.Item color="label">
<u>Concentrations</u>
</LabeledList.Item>
<LabeledList.Item label={'Node 1 (' + data.node1_dir + ')'}>
<NumberInput
animated
value={data.node1_concentration}
unit="%"
width="60px"
minValue={0}
maxValue={100}
stepPixelSize={2}
onDrag={(e, value) =>
act('node1', {
concentration: value,
})
}
/>
</LabeledList.Item>
<LabeledList.Item label={'Node 2 (' + data.node2_dir + ')'}>
<NumberInput
animated
value={data.node2_concentration}
unit="%"
width="60px"
minValue={0}
maxValue={100}
stepPixelSize={2}
onDrag={(e, value) =>
act('node2', {
concentration: value,
})
}
/>
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,119 +0,0 @@
import { flow } from 'common/fp';
import { filter, sortBy } from 'common/collections';
import { useBackend, useSharedState } from '../backend';
import { Box, Button, Flex, Input, Section, Dropdown } from '../components';
import { Window } from '../layouts';
import { Materials } from './ExosuitFabricator';
import { createSearch, toTitleCase } from 'common/string';
const canBeMade = (recipe, materials, mult = 1) => {
if (recipe.requirements === null) {
return true;
}
let recipeRequiredMaterials = Object.keys(recipe.requirements);
for (let mat_id of recipeRequiredMaterials) {
let material = materials.find((val) => val.name === mat_id);
if (!material) {
continue; // yes, if we cannot find the material, we just ignore it :V
}
if (material.amount < recipe.requirements[mat_id] * mult) {
return false;
}
}
return true;
};
export const Autolathe = (props, context) => {
const { act, data } = useBackend(context);
const { recipes, busy, materials, categories } = data;
const [category, setCategory] = useSharedState(context, 'category', 0);
const [searchText, setSearchText] = useSharedState(
context,
'search_text',
''
);
const testSearch = createSearch(searchText, (recipe) => recipe.name);
const recipesToShow = flow([
filter((recipe) => recipe.category === categories[category]),
searchText && filter(testSearch),
sortBy((recipe) => recipe.name.toLowerCase()),
])(recipes);
return (
<Window width={550} height={700}>
<Window.Content scrollable>
<Section title="Materials">
<Materials disableEject />
</Section>
<Section
title="Recipes"
buttons={
<Dropdown
width="190px"
options={categories}
selected={categories[category]}
onSelected={(val) => setCategory(categories.indexOf(val))}
/>
}>
<Input
fluid
placeholder="Search for..."
onInput={(e, v) => setSearchText(v)}
mb={1}
/>
{recipesToShow.map((recipe) => (
<Flex justify="space-between" align="center" key={recipe.ref}>
<Flex.Item>
<Button
color={(recipe.hidden && 'red') || null}
icon="hammer"
iconSpin={busy === recipe.name}
disabled={!canBeMade(recipe, materials, 1)}
onClick={() => act('make', { make: recipe.ref })}>
{toTitleCase(recipe.name)}
</Button>
{(!recipe.is_stack && (
<Box as="span">
<Button
color={(recipe.hidden && 'red') || null}
disabled={!canBeMade(recipe, materials, 5)}
onClick={() =>
act('make', { make: recipe.ref, multiplier: 5 })
}>
x5
</Button>
<Button
color={(recipe.hidden && 'red') || null}
disabled={!canBeMade(recipe, materials, 10)}
onClick={() =>
act('make', { make: recipe.ref, multiplier: 10 })
}>
x10
</Button>
</Box>
)) ||
null}
</Flex.Item>
<Flex.Item>
{(recipe.requirements &&
Object.keys(recipe.requirements)
.map(
(mat) =>
toTitleCase(mat) + ': ' + recipe.requirements[mat]
)
.join(', ')) || <Box>No resources required.</Box>}
</Flex.Item>
</Flex>
))}
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,97 +0,0 @@
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, LabeledList, ProgressBar, Section, Table } from '../components';
import { Window } from '../layouts';
export const Batteryrack = (props, context) => {
const { act, data } = useBackend(context);
const {
mode,
transfer_max,
output_load,
input_load,
equalise,
blink_tick,
cells_max,
cells_cur,
cells_list,
} = data;
return (
<Window width={500} height={430} resizable>
<Window.Content scrollable>
<Section title="Controls">
<LabeledList>
<LabeledList.Item label="Current Mode">
{(mode === 1 && <Box color="good">OUTPUT ONLY</Box>) ||
(mode === 2 && <Box color="good">INPUT ONLY</Box>) ||
(mode === 3 && <Box color="good">INPUT AND OUTPUT</Box>) || (
<Box color="bad">OFFLINE</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Input Status">
<AnimatedNumber value={input_load} /> / {transfer_max} W
</LabeledList.Item>
<LabeledList.Item label="Output Status">
<AnimatedNumber value={output_load} /> / {transfer_max} W
</LabeledList.Item>
<LabeledList.Item label="Control Panel">
<Button
content="OFF"
selected={mode === 0}
onClick={() => act('disable')}
/>
<Button
content="OUT"
selected={mode === 1}
onClick={() => act('enable', { enable: 1 })}
/>
<Button
content="IN"
selected={mode === 2}
onClick={() => act('enable', { enable: 2 })}
/>
<Button
content="IN/OUT"
selected={mode === 3}
onClick={() => act('enable', { enable: 3 })}
/>
{(equalise && (
<Button
content="EQ"
color={blink_tick ? 'red' : 'yellow'}
onClick={() => act('equaliseoff')}
/>
)) || <Button content="EQ" onClick={() => act('equaliseon')} />}
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Cells">
<Table>
{cells_list.map((cell) => (
<Table.Row key={cell.slot}>
<Table.Cell collapsing>Cell {cell.slot}</Table.Cell>
<Table.Cell>
<ProgressBar
value={cell.used ? cell.percentage : 100}
minValue={0}
maxValue={100}
color={cell.used ? 'good' : 'bad'}>
{cell.used ? cell.percentage + '%' : 'N/C'}
</ProgressBar>
</Table.Cell>
<Table.Cell collapsing>
<Button
icon="eject"
disabled={!cell.used}
onClick={() => act('ejectcell', { ejectcell: cell.id })}
/>
</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,55 +0,0 @@
import { toFixed, round } from 'common/math';
import { useBackend } from '../backend';
import { Box, Button, Icon, LabeledList, NumberInput, Section } from '../components';
import { Window } from '../layouts';
export const BeaconLocator = (props, context) => {
const { act, data } = useBackend(context);
const { scan_ticks, degrees, rawfreq, minFrequency, maxFrequency } = data;
return (
<Window width={300} height={220}>
<Window.Content>
<Section title="Beacon Locator">
{(scan_ticks && <Box color="label">Scanning...</Box>) || null}
{(degrees && (
<Box textAlign="center">
<Box textAlign="center">
<Icon size={4} name="arrow-up" rotation={degrees} />
</Box>
Locked on. Follow the arrow.
</Box>
)) || <Box color="average">No lock.</Box>}
<Button
mt={1}
mb={1}
fluid
icon="broadcast-tower"
onClick={() => act('reset_tracking')}>
Reset tracker
</Button>
<LabeledList>
<LabeledList.Item label="Frequency">
<NumberInput
animated
unit="kHz"
step={0.2}
stepPixelSize={10}
minValue={minFrequency / 10}
maxValue={maxFrequency / 10}
value={rawfreq / 10}
format={(value) => toFixed(value, 1)}
onDrag={(e, value) =>
act('setFrequency', {
freq: round(value * 10),
})
}
/>
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -1,195 +0,0 @@
import { createSearch } from 'common/string';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Collapsible, Dropdown, Flex, Input, Section } from '../components';
import { Window } from '../layouts';
const sortTypes = {
'Alphabetical': (a, b) => a - b,
'By availability': (a, b) => -(a.affordable - b.affordable),
'By price': (a, b) => a.price - b.price,
};
export const Biogenerator = (props, context) => {
const { act, data } = useBackend(context);
return (
<Window width={400} height={450} resizable>
<Window.Content className="Layout__content--flexColumn" scrollable>
{(data.processing && (
<Section title="Processing">
The biogenerator is processing reagents!
</Section>
)) || (
<Fragment>
<Section>
{data.points} points available.
<Button ml={1} icon="blender" onClick={() => act('activate')}>
Activate
</Button>
<Button
ml={1}
icon="eject"
disabled={!data.beaker}
onClick={() => act('detach')}>
Eject Beaker
</Button>
</Section>
<BiogeneratorSearch />
<BiogeneratorItems />
</Fragment>
)}
</Window.Content>
</Window>
);
};
const BiogeneratorItems = (props, context) => {
const { act, data } = useBackend(context);
const { points, items } = data;
// Search thingies
const [searchText, _setSearchText] = useLocalState(context, 'search', '');
const [sortOrder, _setSortOrder] = useLocalState(
context,
'sort',
'Alphabetical'
);
const [descending, _setDescending] = useLocalState(
context,
'descending',
false
);
const searcher = createSearch(searchText, (item) => {
return item[0];
});
let has_contents = false;
let contents = Object.entries(items).map((kv, _i) => {
let items_in_cat = Object.entries(kv[1])
.filter(searcher)
.map((kv2) => {
kv2[1].affordable = points >= kv2[1].price / data.build_eff;
return kv2[1];
})
.sort(sortTypes[sortOrder]);
if (items_in_cat.length === 0) {
return;
}
if (descending) {
items_in_cat = items_in_cat.reverse();
}
has_contents = true;
return (
<BiogeneratorItemsCategory
key={kv[0]}
title={kv[0]}
items={items_in_cat}
/>
);
});
return (
<Flex.Item grow="1" overflow="auto">
<Section>
{has_contents ? (
contents
) : (
<Box color="label">No items matching your criteria was found!</Box>
)}
</Section>
</Flex.Item>
);
};
const BiogeneratorSearch = (props, context) => {
const [_searchText, setSearchText] = useLocalState(context, 'search', '');
const [_sortOrder, setSortOrder] = useLocalState(context, 'sort', '');
const [descending, setDescending] = useLocalState(
context,
'descending',
false
);
return (
<Box mb="0.5rem">
<Flex width="100%">
<Flex.Item grow="1" mr="0.5rem">
<Input
placeholder="Search by item name.."
width="100%"
onInput={(_e, value) => setSearchText(value)}
/>
</Flex.Item>
<Flex.Item basis="30%">
<Dropdown
selected="Alphabetical"
options={Object.keys(sortTypes)}
width="100%"
lineHeight="19px"
onSelected={(v) => setSortOrder(v)}
/>
</Flex.Item>
<Flex.Item>
<Button
icon={descending ? 'arrow-down' : 'arrow-up'}
height="19px"
tooltip={descending ? 'Descending order' : 'Ascending order'}
tooltipPosition="bottom-end"
ml="0.5rem"
onClick={() => setDescending(!descending)}
/>
</Flex.Item>
</Flex>
</Box>
);
};
const canBuyItem = (item, data) => {
if (!item.affordable) {
return false;
}
if (item.reagent && !data.beaker) {
return false;
}
return true;
};
const BiogeneratorItemsCategory = (properties, context) => {
const { act, data } = useBackend(context);
const { title, items, ...rest } = properties;
return (
<Collapsible open title={title} {...rest}>
{items.map((item) => (
<Box key={item.name}>
<Box
display="inline-block"
verticalAlign="middle"
lineHeight="20px"
style={{
float: 'left',
}}>
{item.name}
</Box>
<Button
disabled={!canBuyItem(item, data)}
content={(item.price / data.build_eff).toLocaleString('en-US')}
width="15%"
textAlign="center"
style={{
float: 'right',
}}
onClick={() =>
act('purchase', {
cat: title,
name: item.name,
})
}
/>
<Box
style={{
clear: 'both',
}}
/>
</Box>
))}
</Collapsible>
);
};

View File

@@ -1,340 +0,0 @@
import { capitalize } from 'common/string';
import { useBackend } from '../backend';
import { Box, ByondUi, Button, Flex, LabeledList, Section, ColorBox } from '../components';
import { Window } from '../layouts';
export const BodyDesigner = (props, context) => {
const { act, data } = useBackend(context);
const { menu, disk, diskStored, activeBodyRecord } = data;
let body = MenuToTemplate[menu];
return (
<Window width={400} height={650}>
<Window.Content>
{disk ? (
<Box>
<Button
icon="save"
content="Save To Disk"
onClick={() => act('savetodisk')}
disabled={!activeBodyRecord}
/>
<Button
icon="save"
content="Load From Disk"
onClick={() => act('loadfromdisk')}
disabled={!diskStored}
/>
<Button
icon="eject"
content="Eject"
onClick={() => act('ejectdisk')}
/>
</Box>
) : null}
{body}
</Window.Content>
</Window>
);
};
const BodyDesignerMain = (props, context) => {
const { act, data } = useBackend(context);
return (
<Section title="Database Functions">
<Button
icon="eye"
content="View Individual Body Records"
onClick={() => act('menu', { menu: 'Body Records' })}
/>
<Button
icon="eye"
content="View Stock Body Records"
onClick={() => act('menu', { menu: 'Stock Records' })}
/>
</Section>
);
};
const BodyDesignerBodyRecords = (props, context) => {
const { act, data } = useBackend(context);
const { bodyrecords } = data;
return (
<Section
title="Body Records"
buttons={
<Button
icon="arrow-left"
content="Back"
onClick={() => act('menu', { menu: 'Main' })}
/>
}>
{bodyrecords.map((record) => (
<Button
icon="eye"
key={record.name}
content={record.name}
onClick={() => act('view_brec', { view_brec: record.recref })}
/>
))}
</Section>
);
};
const BodyDesignerStockRecords = (props, context) => {
const { act, data } = useBackend(context);
const { stock_bodyrecords } = data;
return (
<Section
title="Stock Records"
buttons={
<Button
icon="arrow-left"
content="Back"
onClick={() => act('menu', { menu: 'Main' })}
/>
}>
{stock_bodyrecords.map((record) => (
<Button
icon="eye"
key={record}
content={record}
onClick={() => act('view_stock_brec', { view_stock_brec: record })}
/>
))}
</Section>
);
};
const BodyDesignerSpecificRecord = (props, context) => {
const { act, data } = useBackend(context);
const { activeBodyRecord, mapRef } = data;
return activeBodyRecord ? (
<Flex direction="column">
<Flex.Item basis="165px">
<Section
title="Specific Record"
buttons={
<Button
icon="arrow-left"
content="Back"
onClick={() => act('menu', { menu: 'Main' })}
/>
}>
<LabeledList>
<LabeledList.Item label="Name">
{activeBodyRecord.real_name}
</LabeledList.Item>
<LabeledList.Item label="Species">
{activeBodyRecord.speciesname}
</LabeledList.Item>
<LabeledList.Item label="Bio. Sex">
<Button
icon="pen"
content={capitalize(activeBodyRecord.gender)}
onClick={() =>
act('href_conversion', {
target_href: 'bio_gender',
target_value: 1,
})
}
/>
</LabeledList.Item>
<LabeledList.Item label="Synthetic">
{activeBodyRecord.synthetic}
</LabeledList.Item>
<LabeledList.Item label="Mind Compat">
{activeBodyRecord.locked}
<Button
ml={1}
icon="eye"
content="View OOC Notes"
disabled={!activeBodyRecord.booc}
onClick={() => act('boocnotes')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
</Flex.Item>
<Flex.Item basis="130px">
<ByondUi
style={{
width: '100%',
height: '128px',
}}
params={{
id: mapRef,
type: 'map',
}}
/>
</Flex.Item>
<Flex.Item basis="300px">
<Section title="Customize" height="300px" style={{ overflow: 'auto' }}>
<LabeledList>
<LabeledList.Item label="Scale">
<Button
icon="pen"
content={activeBodyRecord.scale}
onClick={() =>
act('href_conversion', {
target_href: 'size_multiplier',
target_value: 1,
})
}
/>
</LabeledList.Item>
{Object.keys(activeBodyRecord.styles).map((key) => {
const style = activeBodyRecord.styles[key];
return (
<LabeledList.Item key={key} label={key}>
{style.styleHref ? (
<Button
icon="pen"
content={style.style}
onClick={() =>
act('href_conversion', {
target_href: style.styleHref,
target_value: 1,
})
}
/>
) : null}
{style.colorHref ? (
<Box>
<Button
icon="pen"
content={style.color}
onClick={() =>
act('href_conversion', {
target_href: style.colorHref,
target_value: 1,
})
}
/>
<ColorBox
verticalAlign="top"
width="32px"
height="20px"
color={style.color}
style={{
border: '1px solid #fff',
}}
/>
</Box>
) : null}
{style.colorHref2 ? (
<Box>
<Button
icon="pen"
content={style.color2}
onClick={() =>
act('href_conversion', {
target_href: style.colorHref2,
target_value: 1,
})
}
/>
<ColorBox
verticalAlign="top"
width="32px"
height="20px"
color={style.color2}
style={{
border: '1px solid #fff',
}}
/>
</Box>
) : null}
</LabeledList.Item>
);
})}
<LabeledList.Item label="Body Markings">
<Button
icon="plus"
content="Add Marking"
onClick={() =>
act('href_conversion', {
target_href: 'marking_style',
target_value: 1,
})
}
/>
<Flex wrap="wrap" justify="center" align="center">
{Object.keys(activeBodyRecord.markings).map((key) => {
const marking = activeBodyRecord.markings[key];
return (
<Flex.Item basis="100%" key={key}>
<Flex>
<Flex.Item>
<Button
mr={0.2}
fluid
icon="times"
color="red"
onClick={() =>
act('href_conversion', {
target_href: 'marking_remove',
target_value: key,
})
}
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
fluid
backgroundColor={marking}
content={key}
onClick={() =>
act('href_conversion', {
target_href: 'marking_color',
target_value: key,
})
}
/>
</Flex.Item>
</Flex>
</Flex.Item>
);
})}
</Flex>
</LabeledList.Item>
</LabeledList>
</Section>
</Flex.Item>
</Flex>
) : (
<Box color="bad">ERROR: Record Not Found!</Box>
);
};
const BodyDesignerOOCNotes = (props, context) => {
const { act, data } = useBackend(context);
const { activeBodyRecord } = data;
return (
<Section
title="Body OOC Notes (This is OOC!)"
height="100%"
scrollable
buttons={
<Button
icon="arrow-left"
content="Back"
onClick={() => act('menu', { menu: 'Specific Record' })}
/>
}
style={{ 'word-break': 'break-all' }}>
{(activeBodyRecord && activeBodyRecord.booc) ||
'ERROR: Body record not found!'}
</Section>
);
};
const MenuToTemplate = {
'Main': <BodyDesignerMain />,
'Body Records': <BodyDesignerBodyRecords />,
'Stock Records': <BodyDesignerStockRecords />,
'Specific Record': <BodyDesignerSpecificRecord />,
'OOC Notes': <BodyDesignerOOCNotes />,
};

View File

@@ -1,485 +0,0 @@
import { round } from 'common/math';
import { Fragment } from 'inferno';
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, Flex, Icon, LabeledList, ProgressBar, Section, Table, Tooltip } from '../components';
import { Window } from '../layouts';
const stats = [
['good', 'Alive'],
['average', 'Unconscious'],
['bad', 'DEAD'],
];
const abnormalities = [
[
'hasBorer',
'bad',
(occupant) =>
'Large growth detected in frontal lobe,' +
' possibly cancerous. Surgical removal is recommended.',
],
['hasVirus', 'bad', (occupant) => 'Viral pathogen detected in blood stream.'],
['blind', 'average', (occupant) => 'Cataracts detected.'],
[
'colourblind',
'average',
(occupant) => 'Photoreceptor abnormalities detected.',
],
['nearsighted', 'average', (occupant) => 'Retinal misalignment detected.'],
/* VOREStation Add */
[
'humanPrey',
'average',
(occupant) => {
return 'Foreign Humanoid(s) detected: ' + occupant.humanPrey;
},
],
[
'livingPrey',
'average',
(occupant) => {
return 'Foreign Creature(s) detected: ' + occupant.livingPrey;
},
],
[
'objectPrey',
'average',
(occupant) => {
return 'Foreign Object(s) detected: ' + occupant.objectPrey;
},
],
/* VOREStation Add End */
];
const damages = [
['Respiratory', 'oxyLoss'],
['Brain', 'brainLoss'],
['Toxin', 'toxLoss'],
['Radiation', 'radLoss'],
['Brute', 'bruteLoss'],
['Genetic', 'cloneLoss'],
['Burn', 'fireLoss'],
['Paralysis', 'paralysis'],
];
const damageRange = {
average: [0.25, 0.5],
bad: [0.5, Infinity],
};
const mapTwoByTwo = (a, c) => {
let result = [];
for (let i = 0; i < a.length; i += 2) {
result.push(c(a[i], a[i + 1], i));
}
return result;
};
const reduceOrganStatus = (A) => {
return A.length > 0
? A.reduce((a, s) =>
a === null ? (
s
) : (
<Fragment>
{a}
{!!s && <Box>{s}</Box>}
</Fragment>
)
)
: null;
};
const germStatus = (i) => {
if (i > 100) {
if (i < 300) {
return 'mild infection';
}
if (i < 400) {
return 'mild infection+';
}
if (i < 500) {
return 'mild infection++';
}
if (i < 700) {
return 'acute infection';
}
if (i < 800) {
return 'acute infection+';
}
if (i < 900) {
return 'acute infection++';
}
if (i >= 900) {
return 'septic';
}
}
return '';
};
export const BodyScanner = (props, context) => {
const { data } = useBackend(context);
const { occupied, occupant = {} } = data;
const body = occupied ? (
<BodyScannerMain occupant={occupant} />
) : (
<BodyScannerEmpty />
);
return (
<Window width={690} height={600} resizable>
<Window.Content scrollable className="Layout__content--flexColumn">
{body}
</Window.Content>
</Window>
);
};
const BodyScannerMain = (props) => {
const { occupant } = props;
return (
<Box>
<BodyScannerMainOccupant occupant={occupant} />
<BodyScannerMainReagents occupant={occupant} />
<BodyScannerMainAbnormalities occupant={occupant} />
<BodyScannerMainDamage occupant={occupant} />
<BodyScannerMainOrgansExternal organs={occupant.extOrgan} />
<BodyScannerMainOrgansInternal organs={occupant.intOrgan} />
</Box>
);
};
const BodyScannerMainOccupant = (props, context) => {
const { act, data } = useBackend(context);
const { occupant } = data;
return (
<Section
title="Occupant"
buttons={
<Fragment>
<Button icon="user-slash" onClick={() => act('ejectify')}>
Eject
</Button>
<Button icon="print" onClick={() => act('print_p')}>
Print Report
</Button>
</Fragment>
}>
<LabeledList>
<LabeledList.Item label="Name">{occupant.name}</LabeledList.Item>
<LabeledList.Item label="Health">
<ProgressBar
min="0"
max={occupant.maxHealth}
value={occupant.health / occupant.maxHealth}
ranges={{
good: [0.5, Infinity],
average: [0, 0.5],
bad: [-Infinity, 0],
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Status" color={stats[occupant.stat][0]}>
{stats[occupant.stat][1]}
</LabeledList.Item>
<LabeledList.Item label="Temperature">
<AnimatedNumber value={round(occupant.bodyTempC, 0)} />
&deg;C,&nbsp;
<AnimatedNumber value={round(occupant.bodyTempF, 0)} />
&deg;F
</LabeledList.Item>
<LabeledList.Item label="Blood Volume">
<AnimatedNumber value={round(occupant.blood.volume, 0)} />{' '}
units&nbsp;(
<AnimatedNumber value={round(occupant.blood.percent, 0)} />
%)
</LabeledList.Item>
{/* VOREStation Add */}
<LabeledList.Item label="Weight">
{round(data.occupant.weight) +
'lbs, ' +
round(data.occupant.weight / 2.20463) +
'kgs'}
</LabeledList.Item>
{/* VOREStation Add End */}
</LabeledList>
</Section>
);
};
const BodyScannerMainReagents = (props) => {
const { occupant } = props;
return (
<Fragment>
<Section title="Blood Reagents">
{occupant.reagents ? (
<Table>
<Table.Row header>
<Table.Cell>Reagent</Table.Cell>
<Table.Cell textAlign="right">Amount</Table.Cell>
</Table.Row>
{occupant.reagents.map((reagent) => (
<Table.Row key={reagent.name}>
<Table.Cell>{reagent.name}</Table.Cell>
<Table.Cell textAlign="right">
{reagent.amount} Units{' '}
{reagent.overdose ? <Box color="bad">OVERDOSING</Box> : null}
</Table.Cell>
</Table.Row>
))}
</Table>
) : (
<Box color="good">No Blood Reagents Detected</Box>
)}
</Section>
<Section title="Stomach Reagents">
{occupant.ingested ? (
<Table>
<Table.Row header>
<Table.Cell>Reagent</Table.Cell>
<Table.Cell textAlign="right">Amount</Table.Cell>
</Table.Row>
{occupant.ingested.map((reagent) => (
<Table.Row key={reagent.name}>
<Table.Cell>{reagent.name}</Table.Cell>
<Table.Cell textAlign="right">
{reagent.amount} Units{' '}
{reagent.overdose ? <Box color="bad">OVERDOSING</Box> : null}
</Table.Cell>
</Table.Row>
))}
</Table>
) : (
<Box color="good">No Stomach Reagents Detected</Box>
)}
</Section>
</Fragment>
);
};
const BodyScannerMainAbnormalities = (props) => {
const { occupant } = props;
let hasAbnormalities =
occupant.hasBorer ||
occupant.blind ||
occupant.colourblind ||
occupant.nearsighted ||
occupant.hasVirus;
/* VOREStation Add */
hasAbnormalities =
hasAbnormalities ||
occupant.humanPrey ||
occupant.livingPrey ||
occupant.objectPrey;
/* VOREStation Add End */
if (!hasAbnormalities) {
return (
<Section title="Abnormalities">
<Box color="label">No abnormalities found.</Box>
</Section>
);
}
return (
<Section title="Abnormalities">
{abnormalities.map((a, i) => {
if (occupant[a[0]]) {
return (
<Box color={a[1]} bold={a[1] === 'bad'}>
{a[2](occupant)}
</Box>
);
}
})}
</Section>
);
};
const BodyScannerMainDamage = (props) => {
const { occupant } = props;
return (
<Section title="Damage">
<Table>
{mapTwoByTwo(damages, (d1, d2, i) => (
<Fragment>
<Table.Row color="label">
<Table.Cell>{d1[0]}:</Table.Cell>
<Table.Cell>{!!d2 && d2[0] + ':'}</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
<BodyScannerMainDamageBar
value={occupant[d1[1]]}
marginBottom={i < damages.length - 2}
/>
</Table.Cell>
<Table.Cell>
{!!d2 && <BodyScannerMainDamageBar value={occupant[d2[1]]} />}
</Table.Cell>
</Table.Row>
</Fragment>
))}
</Table>
</Section>
);
};
const BodyScannerMainDamageBar = (props) => {
return (
<ProgressBar
min="0"
max="100"
value={props.value / 100}
mt="0.5rem"
mb={!!props.marginBottom && '0.5rem'}
ranges={damageRange}>
{round(props.value, 0)}
</ProgressBar>
);
};
const BodyScannerMainOrgansExternal = (props) => {
if (props.organs.length === 0) {
return (
<Section title="External Organs">
<Box color="label">N/A</Box>
</Section>
);
}
return (
<Section title="External Organs">
<Table>
<Table.Row header>
<Table.Cell>Name</Table.Cell>
<Table.Cell textAlign="center">Damage</Table.Cell>
<Table.Cell textAlign="right">Injuries</Table.Cell>
</Table.Row>
{props.organs.map((o, i) => (
<Table.Row key={i} textTransform="capitalize">
<Table.Cell width="33%">{o.name}</Table.Cell>
<Table.Cell textAlign="center" q>
<ProgressBar
min="0"
max={o.maxHealth}
mt={i > 0 && '0.5rem'}
value={o.totalLoss / 100}
ranges={damageRange}>
<Box float="left" inline>
{!!o.bruteLoss && (
<Box inline position="relative">
<Icon name="bone" />
{round(o.bruteLoss, 0)}&nbsp;
<Tooltip position="top" content="Brute damage" />
</Box>
)}
{!!o.fireLoss && (
<Box inline position="relative">
<Icon name="fire" />
{round(o.fireLoss, 0)}
<Tooltip position="top" content="Burn damage" />
</Box>
)}
</Box>
<Box inline>{round(o.totalLoss, 0)}</Box>
</ProgressBar>
</Table.Cell>
<Table.Cell textAlign="right" width="33%">
<Box color="average" inline>
{reduceOrganStatus([
o.internalBleeding && 'Internal bleeding',
!!o.status.bleeding && 'External bleeding',
o.lungRuptured && 'Ruptured lung',
o.destroyed && 'Destroyed',
!!o.status.broken && o.status.broken,
germStatus(o.germ_level),
!!o.open && 'Open incision',
])}
</Box>
<Box inline>
{reduceOrganStatus([
!!o.status.splinted && 'Splinted',
!!o.status.robotic && 'Robotic',
!!o.status.dead && <Box color="bad">DEAD</Box>,
])}
{reduceOrganStatus(
o.implants.map((s) => (s.known ? s.name : 'Unknown object'))
)}
</Box>
</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
);
};
const BodyScannerMainOrgansInternal = (props) => {
if (props.organs.length === 0) {
return (
<Section title="Internal Organs">
<Box color="label">N/A</Box>
</Section>
);
}
return (
<Section title="Internal Organs">
<Table>
<Table.Row header>
<Table.Cell>Name</Table.Cell>
<Table.Cell textAlign="center">Damage</Table.Cell>
<Table.Cell textAlign="right">Injuries</Table.Cell>
</Table.Row>
{props.organs.map((o, i) => (
<Table.Row key={i} textTransform="capitalize">
<Table.Cell width="33%">{o.name}</Table.Cell>
<Table.Cell textAlign="center">
<ProgressBar
min="0"
max={o.maxHealth}
value={o.damage / 100}
mt={i > 0 && '0.5rem'}
ranges={damageRange}>
{round(o.damage, 0)}
</ProgressBar>
</Table.Cell>
<Table.Cell textAlign="right" width="33%">
<Box color="average" inline>
{reduceOrganStatus([
germStatus(o.germ_level),
!!o.inflamed && 'Appendicitis detected.',
])}
</Box>
<Box inline>
{reduceOrganStatus([
o.robotic === 1 && 'Robotic',
o.robotic === 2 && 'Assisted',
!!o.dead && <Box color="bad">DEAD</Box>,
])}
</Box>
</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
);
};
const BodyScannerEmpty = () => {
return (
<Section textAlign="center" flexGrow="1">
<Flex height="100%">
<Flex.Item grow="1" align="center" color="label">
<Icon name="user-slash" mb="0.5rem" size="5" />
<br />
No occupant detected.
</Flex.Item>
</Flex>
</Section>
);
};

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