This commit is contained in:
silicons
2021-04-25 14:17:37 -07:00
parent cf69e0fb69
commit 12894384ff
119 changed files with 12403 additions and 1483 deletions
+39 -11
View File
@@ -1,15 +1,43 @@
rules:
## Enforce a maximum cyclomatic complexity allowed in a program
complexity: [error, { max: 25 }]
# complexity: [warn, { max: 25 }]
## Enforce consistent brace style for blocks
brace-style: [error, stroustrup, { allowSingleLine: false }]
# brace-style: [warn, stroustrup, { allowSingleLine: false }]
## Enforce the consistent use of either backticks, double, or single quotes
quotes: [error, single, {
avoidEscape: true,
allowTemplateLiterals: true,
}]
react/jsx-closing-bracket-location: [error, {
selfClosing: after-props,
nonEmpty: after-props,
}]
react/display-name: error
# quotes: [warn, single, {
# avoidEscape: true,
# allowTemplateLiterals: true,
# }]
# react/jsx-closing-bracket-location: [warn, {
# selfClosing: after-props,
# nonEmpty: after-props,
# }]
# react/display-name: warn
## Radar
## ------------------------------------------------------
# radar/cognitive-complexity: warn
radar/max-switch-cases: warn
radar/no-all-duplicated-branches: warn
radar/no-collapsible-if: warn
radar/no-collection-size-mischeck: warn
radar/no-duplicate-string: warn
radar/no-duplicated-branches: warn
radar/no-element-overwrite: warn
radar/no-extra-arguments: warn
radar/no-identical-conditions: warn
radar/no-identical-expressions: warn
radar/no-identical-functions: warn
radar/no-inverted-boolean-check: warn
radar/no-one-iteration-loop: warn
radar/no-redundant-boolean: warn
radar/no-redundant-jump: warn
radar/no-same-line-conditional: warn
radar/no-small-switch: warn
radar/no-unused-collection: warn
radar/no-use-of-empty-return-value: warn
radar/no-useless-catch: warn
radar/prefer-immediate-return: warn
radar/prefer-object-literal: warn
radar/prefer-single-boolean-return: warn
radar/prefer-while: warn
+2 -4
View File
@@ -9,9 +9,8 @@ env:
es6: true
browser: true
node: true
globals:
Byond: readonly
plugins:
- radar
- react
settings:
react:
@@ -20,7 +19,6 @@ rules:
## Possible Errors
## ----------------------------------------
## Enforce “for” loop update clause moving the counter in the right
## direction.
# for-direction: error
@@ -509,7 +507,7 @@ rules:
## Require braces around arrow function bodies
# arrow-body-style: error
## Require parentheses around arrow function arguments
arrow-parens: [error, as-needed]
# arrow-parens: [error, as-needed]
## Enforce consistent spacing before and after the arrow in arrow functions
arrow-spacing: [error, { before: true, after: true }]
## Require super() calls in constructors
+12
View File
@@ -0,0 +1,12 @@
arrowParens: always
bracketSpacing: true
endOfLine: lf
jsxBracketSameLine: true
jsxSingleQuote: false
printWidth: 80
proseWrap: preserve
quoteProps: preserve
semi: true
singleQuote: true
tabWidth: 2
trailingComma: es5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require eslint/lib/api.js
require(absPnpApiPath).setup();
}
}
// Defer to the real eslint/lib/api.js your application uses
module.exports = absRequire(`eslint/lib/api.js`);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "eslint",
"version": "7.19.0-pnpify",
"version": "7.21.0-pnpify",
"main": "./lib/api.js",
"type": "commonjs"
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsc.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsc.js your application uses
module.exports = absRequire(`typescript/lib/tsc.js`);
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
const moduleWrapper = tsserver => {
const {isAbsolute} = require(`path`);
const pnpApi = require(`pnpapi`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
return `${locator.name}@${locator.reference}`;
}));
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
// doesn't understand. This layer makes sure to remove the protocol
// before forwarding it to TS, and to add it back on all returned paths.
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || str.match(/\$\$virtual\//))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes is much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
// file instances instead of the real ones.
//
// We only do this to modules owned by the the dependency tree roots.
// This avoids breaking the resolution when jumping inside a vendor
// with peer dep (otherwise jumping into react-dom would show resolution
// errors on react).
//
const resolved = pnpApi.resolveVirtual(str);
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) {
str = resolved;
}
}
str = str.replace(/\\/g, `/`)
str = str.replace(/^\/?/, `/`);
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
// VSCode only adds it automatically for supported schemes,
// so we have to do it manually for the `zip` scheme.
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
if (str.match(/\.zip\//)) {
str = `${isVSCode ? `^` : ``}zip:${str}`;
}
}
return str;
}
function fromEditorPath(str) {
return process.platform === `win32`
? str.replace(/^\^?zip:\//, ``)
: str.replace(/^\^?zip:/, ``);
}
// And here is the point where we hijack the VSCode <-> TS communications
// by adding ourselves in the middle. We locate everything that looks
// like an absolute path of ours and normalize it.
const Session = tsserver.server.Session;
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
let isVSCode = false;
return Object.assign(Session.prototype, {
onMessage(/** @type {string} */ message) {
const parsedMessage = JSON.parse(message)
if (
parsedMessage != null &&
typeof parsedMessage === `object` &&
parsedMessage.arguments &&
parsedMessage.arguments.hostInfo === `vscode`
) {
isVSCode = true;
}
return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
return typeof value === `string` ? fromEditorPath(value) : value;
}));
},
send(/** @type {any} */ msg) {
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
return typeof value === `string` ? toEditorPath(value) : value;
})));
}
});
};
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/tsserver.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/tsserver.js your application uses
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env node
const {existsSync} = require(`fs`);
const {createRequire, createRequireFromPath} = require(`module`);
const {resolve} = require(`path`);
const relPnpApiPath = "../../../../.pnp.js";
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
// Setup the environment to be able to require typescript/lib/typescript.js
require(absPnpApiPath).setup();
}
}
// Defer to the real typescript/lib/typescript.js your application uses
module.exports = absRequire(`typescript/lib/typescript.js`);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "typescript",
"version": "4.1.5-pnpify",
"version": "4.2.3-pnpify",
"main": "./lib/typescript.js",
"type": "commonjs"
}
+1 -5
View File
@@ -1,21 +1,17 @@
enableScripts: false
logFilters:
## DISABLED_BUILD_SCRIPTS
- code: YN0004
level: discard
## INCOMPATIBLE_OS - fsevents junk
- code: YN0062
level: discard
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
preferAggregateCacheInfo: true
preferInteractive: true
yarnPath: .yarn/releases/yarn-2.4.0.cjs
yarnPath: .yarn/releases/yarn-2.4.1.cjs
+8 -8
View File
@@ -8,13 +8,13 @@ const createBabelConfig = options => {
const { mode, presets = [], plugins = [] } = options;
return {
presets: [
['@babel/preset-typescript', {
[require.resolve('@babel/preset-typescript'), {
allowDeclareFields: true,
}],
['@babel/preset-env', {
[require.resolve('@babel/preset-env'), {
modules: 'commonjs',
useBuiltIns: 'entry',
corejs: '3.8',
corejs: '3.10',
spec: false,
loose: true,
targets: [],
@@ -22,13 +22,13 @@ const createBabelConfig = options => {
...presets,
],
plugins: [
['@babel/plugin-proposal-class-properties', {
[require.resolve('@babel/plugin-proposal-class-properties'), {
loose: true,
}],
'@babel/plugin-transform-jscript',
'babel-plugin-inferno',
'babel-plugin-transform-remove-console',
'common/string.babel-plugin.cjs',
require.resolve('@babel/plugin-transform-jscript'),
require.resolve('babel-plugin-inferno'),
require.resolve('babel-plugin-transform-remove-console'),
require.resolve('common/string.babel-plugin.cjs'),
...plugins,
],
};
+4 -3
View File
@@ -67,7 +67,7 @@ task-lint() {
cd "${base_dir}"
yarn run tsc
echo "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs "${@}"
yarn run eslint packages --ext .js,.cjs,.ts,.tsx "${@}"
echo "tgui: eslint check passed"
}
@@ -89,8 +89,9 @@ task-clean() {
rm -rf .yarn/cache
rm -rf .yarn/unplugged
rm -rf .yarn/webpack
rm -rf .yarn/build-state.yml
rm -rf .yarn/install-state.gz
rm -f .yarn/build-state.yml
rm -f .yarn/install-state.gz
rm -f .yarn/install-target
rm -f .pnp.js
## NPM artifacts
rm -rf **/node_modules
+4 -3
View File
@@ -54,7 +54,7 @@ function task-dev-server {
function task-lint {
yarn run tsc
Write-Output "tgui: type check passed"
yarn run eslint packages --ext .js,.jsx,.ts,.tsx,.cjs,.mjs @Args
yarn run eslint packages --ext ".js,.cjs,.ts,.tsx" @Args
Write-Output "tgui: eslint check passed"
}
@@ -72,8 +72,9 @@ function task-clean {
Remove-Quiet -Recurse -Force ".yarn\cache"
Remove-Quiet -Recurse -Force ".yarn\unplugged"
Remove-Quiet -Recurse -Force ".yarn\webpack"
Remove-Quiet -Recurse -Force ".yarn\build-state.yml"
Remove-Quiet -Recurse -Force ".yarn\install-state.gz"
Remove-Quiet -Force ".yarn\build-state.yml"
Remove-Quiet -Force ".yarn\install-state.gz"
Remove-Quiet -Force ".yarn\install-target"
Remove-Quiet -Force ".pnp.js"
## NPM artifacts
Get-ChildItem -Path "." -Include "node_modules" -Recurse -File:$false | Remove-Item -Recurse -Force
+17
View File
@@ -5,6 +5,23 @@
*/
declare global {
// Webpack asset modules.
// Should match extensions used in webpack config.
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
type ByondType = {
/**
* True if javascript is running in BYOND.
+4 -4
View File
@@ -1,14 +1,14 @@
module.exports = {
roots: ['<rootDir>/packages'],
testMatch: [
'<rootDir>/packages/**/__tests__/*.{js,jsx,ts,tsx}',
'<rootDir>/packages/**/*.{spec,test}.{js,jsx,ts,tsx}',
'<rootDir>/packages/**/__tests__/*.{js,ts,tsx}',
'<rootDir>/packages/**/*.{spec,test}.{js,ts,tsx}',
],
testEnvironment: 'jsdom',
testRunner: require.resolve('jest-circus/runner'),
transform: {
'^.+\\.(js|jsx|ts|tsx|cjs|mjs)$': require.resolve('babel-jest'),
'^.+\\.(js|cjs|ts|tsx)$': require.resolve('babel-jest'),
},
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
moduleFileExtensions: ['js', 'cjs', 'ts', 'tsx', 'json'],
resetMocks: true,
};
+20 -19
View File
@@ -6,38 +6,39 @@
"packages/*"
],
"dependencies": {
"@babel/core": "^7.12.17",
"@babel/eslint-parser": "^7.12.17",
"@babel/plugin-proposal-class-properties": "^7.12.13",
"@babel/core": "^7.13.15",
"@babel/eslint-parser": "^7.13.14",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-transform-jscript": "^7.12.13",
"@babel/preset-env": "^7.12.17",
"@babel/preset-typescript": "^7.12.17",
"@types/jest": "^26.0.20",
"@types/jsdom": "^16.2.6",
"@types/node": "^14.14.31",
"@typescript-eslint/parser": "^4.15.1",
"@babel/preset-env": "^7.13.15",
"@babel/preset-typescript": "^7.13.0",
"@types/jest": "^26.0.22",
"@types/jsdom": "^16.2.10",
"@types/node": "^14.14.41",
"@typescript-eslint/parser": "^4.22.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-inferno": "^6.1.1",
"babel-plugin-inferno": "^6.2.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"common": "workspace:*",
"css-loader": "^5.0.2",
"eslint": "^7.20.0",
"eslint-plugin-react": "^7.22.0",
"css-loader": "^5.2.2",
"eslint": "^7.24.0",
"eslint-plugin-radar": "^0.2.1",
"eslint-plugin-react": "^7.23.2",
"file-loader": "^6.2.0",
"inferno": "^7.4.8",
"jest": "^26.6.3",
"jest-circus": "^26.6.3",
"jsdom": "^16.4.0",
"mini-css-extract-plugin": "^1.3.8",
"jsdom": "^16.5.3",
"mini-css-extract-plugin": "^1.4.1",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"typescript": "^4.1.5",
"typescript": "^4.2.4",
"url-loader": "^4.1.1",
"webpack": "^5.23.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0"
"webpack": "^5.33.2",
"webpack-bundle-analyzer": "^4.4.1",
"webpack-cli": "^4.6.0"
}
}
+20
View File
@@ -0,0 +1,20 @@
import { range, zip } from "./collections";
// Type assertions, these will lint if the types are wrong.
const _zip1: [string, number] = zip(["a"], [1])[0];
describe("range", () => {
test("range(0, 5)", () => {
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
});
});
describe("zip", () => {
test("zip(['a', 'b', 'c'], [1, 2, 3, 4])", () => {
expect(zip(["a", "b", "c"], [1, 2, 3, 4])).toEqual([
["a", 1],
["b", 2],
["c", 3],
]);
});
});
+288
View File
@@ -0,0 +1,288 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
/**
* Converts a given collection to an array.
*
* - Arrays are returned unmodified;
* - If object was provided, keys will be discarded;
* - Everything else will result in an empty array.
*
* @returns {any[]}
*/
export const toArray = collection => {
if (Array.isArray(collection)) {
return collection;
}
if (typeof collection === 'object') {
const hasOwnProperty = Object.prototype.hasOwnProperty;
const result = [];
for (let i in collection) {
if (hasOwnProperty.call(collection, i)) {
result.push(collection[i]);
}
}
return result;
}
return [];
};
/**
* Converts a given object to an array, and appends a key to every
* object inside of that array.
*
* Example input (object):
* ```
* {
* 'Foo': { info: 'Hello world!' },
* 'Bar': { info: 'Hello world!' },
* }
* ```
*
* Example output (array):
* ```
* [
* { key: 'Foo', info: 'Hello world!' },
* { key: 'Bar', info: 'Hello world!' },
* ]
* ```
*
* @template T
* @param {{ [key: string]: T }} obj Object, or in DM terms, an assoc array
* @param {string} keyProp Property, to which key will be assigned
* @returns {T[]} Array of keyed objects
*/
export const toKeyedArray = (obj, keyProp = 'key') => {
return map((item, key) => ({
[keyProp]: key,
...item,
}))(obj);
};
/**
* Iterates over elements of collection, returning an array of all elements
* iteratee returns truthy for. The predicate is invoked with three
* arguments: (value, index|key, collection).
*
* If collection is 'null' or 'undefined', it will be returned "as is"
* without emitting any errors (which can be useful in some cases).
*
* @returns {any[]}
*/
export const filter = iterateeFn => collection => {
if (collection === null || collection === undefined) {
return collection;
}
if (Array.isArray(collection)) {
const result = [];
for (let i = 0; i < collection.length; i++) {
const item = collection[i];
if (iterateeFn(item, i, collection)) {
result.push(item);
}
}
return result;
}
throw new Error(`filter() can't iterate on type ${typeof collection}`);
};
/**
* Creates an array of values by running each element in collection
* thru an iteratee function. The iteratee is invoked with three
* arguments: (value, index|key, collection).
*
* If collection is 'null' or 'undefined', it will be returned "as is"
* without emitting any errors (which can be useful in some cases).
*
* @returns {any[]}
*/
export const map = iterateeFn => collection => {
if (collection === null || collection === undefined) {
return collection;
}
if (Array.isArray(collection)) {
const result = [];
for (let i = 0; i < collection.length; i++) {
result.push(iterateeFn(collection[i], i, collection));
}
return result;
}
if (typeof collection === 'object') {
const hasOwnProperty = Object.prototype.hasOwnProperty;
const result = [];
for (let i in collection) {
if (hasOwnProperty.call(collection, i)) {
result.push(iterateeFn(collection[i], i, collection));
}
}
return result;
}
throw new Error(`map() can't iterate on type ${typeof collection}`);
};
const COMPARATOR = (objA, objB) => {
const criteriaA = objA.criteria;
const criteriaB = objB.criteria;
const length = criteriaA.length;
for (let i = 0; i < length; i++) {
const a = criteriaA[i];
const b = criteriaB[i];
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
}
return 0;
};
/**
* Creates an array of elements, sorted in ascending order by the results
* of running each element in a collection thru each iteratee.
*
* Iteratees are called with one argument (value).
*
* @returns {any[]}
*/
export const sortBy = (...iterateeFns) => array => {
if (!Array.isArray(array)) {
return array;
}
let length = array.length;
// Iterate over the array to collect criteria to sort it by
let mappedArray = [];
for (let i = 0; i < length; i++) {
const value = array[i];
mappedArray.push({
criteria: iterateeFns.map(fn => fn(value)),
value,
});
}
// Sort criteria using the base comparator
mappedArray.sort(COMPARATOR);
// Unwrap values
while (length--) {
mappedArray[length] = mappedArray[length].value;
}
return mappedArray;
};
export const sort = sortBy();
/**
* Returns a range of numbers from start to end, exclusively.
* For example, range(0, 5) will return [0, 1, 2, 3, 4].
*/
export const range = (start: number, end: number): number[] =>
new Array(end - start).fill(null).map((_, index) => index + start);
/**
* A fast implementation of reduce.
*/
export const reduce = (reducerFn, initialValue) => array => {
const length = array.length;
let i;
let result;
if (initialValue === undefined) {
i = 1;
result = array[0];
}
else {
i = 0;
result = initialValue;
}
for (; i < length; i++) {
result = reducerFn(result, array[i], i, array);
}
return result;
};
/**
* Creates a duplicate-free version of an array, using SameValueZero for
* equality comparisons, in which only the first occurrence of each element
* is kept. The order of result values is determined by the order they occur
* in the array.
*
* It accepts iteratee which is invoked for each element in array to generate
* the criterion by which uniqueness is computed. The order of result values
* is determined by the order they occur in the array. The iteratee is
* invoked with one argument: value.
*/
/* eslint-disable indent */
export const uniqBy = <T extends unknown>(
iterateeFn?: (value: T) => unknown
) => (array: T[]) => {
const { length } = array;
const result = [];
const seen = iterateeFn ? [] : result;
let index = -1;
outer:
while (++index < length) {
let value: T | 0 = array[index];
const computed = iterateeFn ? iterateeFn(value) : value;
value = value !== 0 ? value : 0;
if (computed === computed) {
let seenIndex = seen.length;
while (seenIndex--) {
if (seen[seenIndex] === computed) {
continue outer;
}
}
if (iterateeFn) {
seen.push(computed);
}
result.push(value);
}
else if (!seen.includes(computed)) {
if (seen !== result) {
seen.push(computed);
}
result.push(value);
}
}
return result;
};
/* eslint-enable indent */
export const uniq = uniqBy();
type Zip<T extends unknown[][]> = {
[I in keyof T]: T[I] extends (infer U)[] ? U : never;
}[];
/**
* Creates an array of grouped elements, the first of which contains
* the first elements of the given arrays, the second of which contains
* the second elements of the given arrays, and so on.
*/
export const zip = <T extends unknown[][]>(...arrays: T): Zip<T> => {
if (arrays.length === 0) {
return;
}
const numArrays = arrays.length;
const numValues = arrays[0].length;
const result = [];
for (let valueIndex = 0; valueIndex < numValues; valueIndex++) {
const entry = [];
for (let arrayIndex = 0; arrayIndex < numArrays; arrayIndex++) {
entry.push(arrays[arrayIndex][valueIndex]);
}
result.push(entry);
}
return result;
};
/**
* This method is like "zip" except that it accepts iteratee to
* specify how grouped values should be combined. The iteratee is
* invoked with the elements of each group.
*
* @returns {any[]}
*/
export const zipWith = iterateeFn => (...arrays) => {
return map(values => iterateeFn(...values))(zip(...arrays));
};
+100
View File
@@ -0,0 +1,100 @@
/**
* @file
* @copyright 2020 Aleksej Komarov
* @license MIT
*/
/**
* Limits a number to the range between 'min' and 'max'.
*/
export const clamp = (value, min, max) => {
return value < min ? min : value > max ? max : value;
};
/**
* Limits a number between 0 and 1.
*/
export const clamp01 = value => {
return value < 0 ? 0 : value > 1 ? 1 : value;
};
/**
* Scales a number to fit into the range between min and max.
*/
export const scale = (value, min, max) => {
return (value - min) / (max - min);
};
/**
* Robust number rounding.
*
* Adapted from Locutus, see: http://locutus.io/php/math/round/
*
* @param {number} value
* @param {number} precision
* @return {number}
*/
export const round = (value, precision) => {
if (!value || isNaN(value)) {
return value;
}
// helper variables
let m, f, isHalf, sgn;
// making sure precision is integer
precision |= 0;
m = Math.pow(10, precision);
value *= m;
// sign of the number
sgn = +(value > 0) | -(value < 0);
// isHalf = value % 1 === 0.5 * sgn;
isHalf = Math.abs(value % 1) >= 0.4999999999854481;
f = Math.floor(value);
if (isHalf) {
// rounds .5 away from zero
value = f + (sgn > 0);
}
return (isHalf ? value : Math.round(value)) / m;
};
/**
* Returns a string representing a number in fixed point notation.
*/
export const toFixed = (value, fractionDigits = 0) => {
return Number(value).toFixed(Math.max(fractionDigits, 0));
};
/**
* Checks whether a value is within the provided range.
*
* Range is an array of two numbers, for example: [0, 15].
*/
export const inRange = (value, range) => {
return range
&& value >= range[0]
&& value <= range[1];
};
/**
* Walks over the object with ranges, comparing value against every range,
* and returns the key of the first matching range.
*
* Range is an array of two numbers, for example: [0, 15].
*/
export const keyOfMatchingRange = (value, ranges) => {
for (let rangeName of Object.keys(ranges)) {
const range = ranges[rangeName];
if (inRange(value, range)) {
return rangeName;
}
}
};
/**
* Get number of digits following the decimal point in a number
*/
export const numberOfDecimalDigits = value => {
if (Math.floor(value) !== value) {
return value.toString().split('.')[1].length || 0;
}
return 0;
};
+1 -1
View File
@@ -11,7 +11,7 @@ export const IMPL_LOCAL_STORAGE = 1;
export const IMPL_INDEXED_DB = 2;
const INDEXED_DB_VERSION = 1;
const INDEXED_DB_NAME = 'tgui-citadel-main';
const INDEXED_DB_NAME = 'tgui';
const INDEXED_DB_STORE_NAME = 'storage-v1';
const READ_ONLY = 'readonly';
+1
View File
@@ -0,0 +1 @@
/dist
+14
View File
@@ -0,0 +1,14 @@
/**
* @file
* @copyright 2021 AnturK https://github.com/AnturK
* @license MIT
*/
module.exports = {
name: 'tgfont',
inputDir: './icons',
outputDir: './dist',
fontTypes: ['woff2', 'eot'],
assetTypes: ['css'],
prefix: 'tg',
};
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 437.4 434.4" style="enable-background:new 0 0 437.4 434.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:0.57;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:0.5;stroke-miterlimit:10;}
.st2{stroke:#000000;stroke-width:18;stroke-miterlimit:10;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g id="Layer_3">
</g>
<path class="st0" d="M41.18,306.18l84.43,84.43l-19.53,20.8c-11.26,11.99-26.1,17.98-40.93,17.98c-14.84,0-29.68-5.99-40.93-17.98
c-22.52-23.97-22.52-63.2,0-87.18L41.18,306.18z"/>
<path class="st0" d="M351.66,149.88L139.65,375.65l-84.43-84.43L269.8,62.7c22.51-23.98,59.34-23.98,81.86,0
C374.17,86.67,374.17,125.9,351.66,149.88z"/>
<path class="st1" d="M426.11,88.46c-24.57-26.71-49.15-53.42-73.72-80.13c-5.87,0.15-12.84-0.21-20.56-1.73
c-3.76-0.74-7.22-1.66-10.37-2.66l0,0c-6.96,7.41-6.96,19.53,0,26.94l79.36,84.52c6.96,7.41,18.34,7.41,25.29,0
C433.07,107.99,433.07,95.87,426.11,88.46z"/>
<path class="st1" d="M248.07,35.83c13.66,16.01,4.9,48.19-19.57,71.87c-24.47,23.68-33.21,29.27-46.88,13.25
c-13.66-16.01-2.33-26.15,22.14-49.83C228.23,47.45,234.41,19.82,248.07,35.83z"/>
<line class="st0" x1="139.93" y1="375.93" x2="139.65" y2="375.65"/>
<line class="st0" x1="125.93" y1="390.93" x2="125.61" y2="390.61"/>
<rect x="191.43" y="-6.09" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -86.5159 210.9892)" class="st2" width="40" height="432.04"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 437.4 434.4" style="enable-background:new 0 0 437.4 434.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:0.57;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:0.5;stroke-miterlimit:10;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g id="Layer_3">
</g>
<path class="st0" d="M41.18,306.18l84.43,84.43l-19.53,20.8c-11.26,11.99-26.1,17.98-40.93,17.98c-14.84,0-29.68-5.99-40.93-17.98
c-22.52-23.97-22.52-63.2,0-87.18L41.18,306.18z"/>
<path class="st0" d="M351.66,149.88L139.65,375.65l-84.43-84.43L269.8,62.7c22.51-23.98,59.34-23.98,81.86,0
C374.17,86.67,374.17,125.9,351.66,149.88z"/>
<path class="st1" d="M426.11,88.46c-24.57-26.71-49.15-53.42-73.72-80.13c-5.87,0.15-12.84-0.21-20.56-1.73
c-3.76-0.74-7.22-1.66-10.37-2.66l0,0c-6.96,7.41-6.96,19.53,0,26.94l79.36,84.52c6.96,7.41,18.34,7.41,25.29,0
C433.07,107.99,433.07,95.87,426.11,88.46z"/>
<path class="st1" d="M248.07,35.83c13.66,16.01,4.9,48.19-19.57,71.87c-24.47,23.68-33.21,29.27-46.88,13.25
c-13.66-16.01-2.33-26.15,22.14-49.83C228.23,47.45,234.41,19.82,248.07,35.83z"/>
<line class="st0" x1="139.93" y1="375.93" x2="139.65" y2="375.65"/>
<line class="st0" x1="125.93" y1="390.93" x2="125.61" y2="390.61"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,8 @@
<?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/ -->

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,6 @@
<?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/ -->

After

Width:  |  Height:  |  Size: 3.4 KiB

+14
View File
@@ -0,0 +1,14 @@
/**
* @file
* @copyright 2021 AnturK https://github.com/AnturK
* @license MIT
*/
// Change working directory to project root
process.chdir(__dirname);
// Silently make a dist folder
try {
require('fs').mkdirSync('dist');
}
catch (err) {}
+11
View File
@@ -0,0 +1,11 @@
{
"private": true,
"name": "tgfont",
"version": "1.0.0",
"dependencies": {
"fantasticon": "^1.1.3"
},
"scripts": {
"build": "node mkdist.cjs && fantasticon --config config.cjs"
}
}
+1 -1
View File
@@ -52,7 +52,7 @@ DreamSeeker.getInstancesByPids = async pids => {
}
if (pidsToResolve.length > 0) {
try {
const command = 'netstat -ano | findstr LISTENING';
const command = 'netstat -ano | findstr TCP | findstr 0.0.0.0:0';
const { stdout } = await promisify(exec)(command, {
// Max buffer of 1MB (default is 200KB)
maxBuffer: 1024 * 1024,
+1 -1
View File
@@ -9,6 +9,6 @@
"glob": "^7.1.6",
"source-map": "^0.7.3",
"stacktrace-parser": "^0.1.10",
"ws": "^7.4.3"
"ws": "^7.4.4"
}
}
+14 -9
View File
@@ -8,7 +8,6 @@ import { createLogger } from 'common/logging.js';
import fs from 'fs';
import os from 'os';
import { basename } from 'path';
import { promisify } from 'util';
import { resolveGlob, resolvePath } from './util.js';
import { regQuery } from './winreg.js';
import { DreamSeeker } from './dreamseeker.js';
@@ -68,7 +67,7 @@ export const findCacheRoot = async () => {
const onCacheRootFound = cacheRoot => {
logger.log(`found cache at '${cacheRoot}'`);
// Plant dummy
// Plant a dummy
fs.closeSync(fs.openSync(cacheRoot + '/dummy', 'w'));
};
@@ -93,15 +92,21 @@ export const reloadByondCache = async bundleDir => {
for (let cacheDir of cacheDirs) {
// Clear garbage
const garbage = await resolveGlob(cacheDir, './*.+(bundle|chunk|hot-update).*');
for (let file of garbage) {
await promisify(fs.unlink)(file);
try {
for (let file of garbage) {
fs.unlinkSync(file);
}
// Copy assets
for (let asset of assets) {
const destination = resolvePath(cacheDir, basename(asset));
fs.writeFileSync(destination, fs.readFileSync(asset));
}
logger.log(`copied ${assets.length} files to '${cacheDir}'`);
}
// Copy assets
for (let asset of assets) {
const destination = resolvePath(cacheDir, basename(asset));
await promisify(fs.copyFile)(asset, destination);
catch (err) {
logger.error(`failed copying to '${cacheDir}'`);
logger.error(err);
}
logger.log(`copied ${assets.length} files to '${cacheDir}'`);
}
// Notify dreamseeker
const dss = await dssPromise;
+2 -2
View File
@@ -33,7 +33,7 @@ export const Panel = (props, context) => {
}
}
return (
<Pane theme={settings.theme === 'default' ? 'light' : settings.theme}>
<Pane theme={settings.theme}>
<Stack fill vertical>
<Stack.Item>
<Section fitted>
@@ -114,7 +114,7 @@ export const Panel = (props, context) => {
const HoboPanel = (props, context) => {
const settings = useSettings(context);
return (
<Pane theme={settings.theme === 'default' ? 'light' : settings.theme}>
<Pane theme={settings.theme}>
<Pane.Content scrollable>
<Button
style={{
+2 -14
View File
@@ -58,13 +58,13 @@ export const MESSAGE_TYPES = [
type: MESSAGE_TYPE_RADIO,
name: 'Radio',
description: 'All departments of radio messages',
selector: '.alert, .minorannounce, .syndradio, .airadio, .entradio, .centcomradio, .aiprivradio, .comradio, .secradio, .engradio, .medradio, .sciradio, .suppradio, .servradio, .radio, .deptradio, .binarysay, .newscaster',
selector: '.alert, .minorannounce, .syndradio, .centcomradio, .aiprivradio, .comradio, .secradio, .engradio, .medradio, .sciradio, .suppradio, .servradio, .radio, .deptradio, .binarysay, .newscaster',
},
{
type: MESSAGE_TYPE_INFO,
name: 'Info',
description: 'Non-urgent messages from the game and items',
selector: '.notice:not(.pm), .adminnotice, .info, .sinister, .cult, .infoplain',
selector: '.notice:not(.pm), .adminnotice, .info, .sinister, .cult, .infoplain, .announce',
},
{
type: MESSAGE_TYPE_WARNING,
@@ -84,18 +84,6 @@ export const MESSAGE_TYPES = [
description: 'The bluewall of global OOC messages',
selector: '.ooc, .adminooc',
},
{
type: 'looc',
name: 'LOOC',
description: 'Local OOC Chat. Less of a bluewall',
selector: '.filter_looc, .looc',
},
{
type: 'antagooc',
name: 'AOOC',
description: 'Antag OOC Chat.',
selector: '.filter_aooc, .antagooc',
},
{
type: MESSAGE_TYPE_ADMINPM,
name: 'Admin PMs',
+1 -3
View File
@@ -176,9 +176,7 @@ class ChatRenderer {
this.highlightColor = null;
return;
}
// citadel update - ig changed to i for flags,
// to fix issues with highlighting only working for every other word.
const allowedRegex = /^[a-z0-9_\-\s]+$/i;
const allowedRegex = /^[a-z0-9_\-\s]+$/ig;
const lines = String(text)
.split(',')
.map(str => str.trim())
+7 -10
View File
@@ -93,8 +93,13 @@ const setupApp = () => {
'pos': '0x0',
'size': '0x0',
});
// Absolutely shit workaround for chatbox visiblity.
based_winset();
// Resize the panel to match the non-browser output
Byond.winget('output').then(output => {
Byond.winset('browseroutput', {
'size': output.size,
});
});
// Enable hot module reloading
if (module.hot) {
@@ -114,12 +119,4 @@ const setupApp = () => {
}
};
const based_winset = async (based_on_what = 'output') => {
// shitty workaround because winget is async.
const winget_output = await Byond.winget(based_on_what);
Byond.winset('browseroutput', {
'size': winget_output["size"],
});
};
setupApp();
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.2.6",
"dompurify": "^2.2.7",
"inferno": "^7.4.8",
"tgui": "workspace:*",
"tgui-dev-server": "workspace:*",
+1 -1
View File
@@ -12,7 +12,7 @@ const initialState = {
fontSize: 13,
fontFamily: FONTS[0],
lineHeight: 1.2,
theme: 'default',
theme: 'light',
adminMusicVolume: 0.5,
highlightText: '',
highlightColor: '#ffdd44',
@@ -287,16 +287,6 @@ em {
font-weight: bold;
}
.looc {
color: #6699CC;
font-weight: bold;
}
.antagooc {
color: #b8002e;
font-weight: bold;
}
.adminobserverooc {
color: #0099cc;
font-weight: bold;
@@ -312,28 +302,11 @@ em {
font-weight: bold;
}
/* @adminobserver compatability*/
.adminobserver {
color: #996600;
font-weight: bold;
}
.admin {
color: #5975da;
font-weight: bold;
}
/* cit-specific mentorsay*/
.mentorsay {
color: #E236D8;
font-weight: bold;
}
.mentorsay_admin {
color: #8A2BE2;
font-weight: bold;
}
.name {
font-weight: bold;
}
@@ -409,6 +382,14 @@ em {
color: #3434fd;
}
.greenteamradio {
color: #34fd34;
}
.yellowteamradio {
color: #fdfd34;
}
.yell {
font-weight: bold;
}
@@ -542,20 +523,6 @@ em {
color: #059223;
}
/* hornichems */
.userlove {
color: #ff42a6;
font-style: italic;
font-weight: bold;
text-shadow: 0 0 6px #82365e;
}
.love {
color: #ff4591;
font-style: italic;
text-shadow: 0 0 6px #994449;
}
.cult {
color: #973e3b;
}
@@ -649,155 +616,12 @@ em {
font-size: 185%;
}
.umbra {
color: #5000A0;
}
.umbra_emphasis {
color: #5000A0;
font-weight: bold;
font-style: italic;
}
.umbra_large {
color: #5000A0;
font-weight: bold;
font-style: italic;
font-size: 3;
}
.deconversion_message {
color: #a947ff;
font-size: 185%;
font-style: italic;
}
/* coggers dosen't exist on tg :( */
/* from decon_msg: font-size: 185% == font-size 3*/
.brass {
color: #BE8700;
}
.heavy_brass {
color: #BE8700;
font-weight: bold;
font-style: italic;
}
.large_brass {
color: #BE8700;
font-size: 185%;
}
.big_brass {
color: #BE8700;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.ratvar {
color: #BE8700;
font-size: 6;
font-weight: bold;
font-style: italic;
}
.alloy {
color: #42474D;
}
.heavy_alloy {
color: #42474D;
font-weight: bold;
font-style: italic;
}
.nezbere_large {
color: #42474D;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.nezbere {
color: #42474D;
font-weight: bold;
font-style: italic;
}
.nezbere_small {
color: #42474D;
}
.sevtug_large {
color: #AF0AAF;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.sevtug {
color: #AF0AAF;
font-weight: bold;
font-style: italic;
}
.sevtug_small {
color: #AF0AAF;
}
.inathneq_large {
color: #1E8CE1;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.inathneq {
color: #1E8CE1;
font-weight: bold;
font-style: italic;
}
.inathneq_small {
color: #1E8CE1;
}
.nzcrentr_large {
color: #DAAA18;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.nzcrentr {
color: #DAAA18;
font-weight: bold;
font-style: italic;
}
.nzcrentr_small {
color: #DAAA18;
}
.neovgre_large {
color: #6E001A;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.neovgre {
color: #6E001A;
font-weight: bold;
font-style: italic;
}
.neovgre_small {
color: #6E001A;
}
.ghostalert {
color: #6600ff;
font-style: italic;
@@ -857,7 +681,7 @@ em {
.command_headset {
font-weight: bold;
font-size: 125%;
font-size: 160%;
}
.small {
@@ -886,14 +710,9 @@ em {
font-size: 185%;
}
.yellowtext {
color: #FFCC00;
font-size: 185%;
}
.clown {
color: #ff70c1;
font-size: 125%;
font-size: 160%;
font-family: "Comic Sans MS", cursive, sans-serif;
font-weight: bold;
}
@@ -909,11 +728,6 @@ em {
font-style: italic;
}
/*jacq */
.spooky {
color: #FF9100;
}
.hypnophrase {
color: #202020;
font-weight: bold;
@@ -943,31 +757,6 @@ em {
}
}
.velvet {
color: #660015;
font-weight: bold;
animation: velvet 5000ms infinite;
animation-direction: alternate;
}
@keyframes velvet {
0% {
color: #400020;
}
40% {
color: #FF0000;
}
50% {
color: #FF8888;
}
60% {
color: #FF0000;
}
100% {
color: #400020;
}
}
.phobia {
color: #dd0000;
font-weight: bold;
@@ -993,6 +782,10 @@ em {
width: auto;
}
.bigicon {
font-size: 2.5em;
}
.memo {
color: #638500;
text-align: center;
@@ -11,7 +11,7 @@ html, body {
}
body {
background: #E0E0E0; /*CIT CHANGE - darkens chatbox a lil*/
background: #fff;
font-family: Verdana, sans-serif;
font-size: 13px;
line-height: 1.2;
@@ -305,16 +305,6 @@ em {
font-weight: bold;
}
.looc {
color: #6699CC;
font-weight: bold;
}
.antagooc {
color: #b8002e;
font-weight: bold;
}
.adminobserverooc {
color: #0099cc;
font-weight: bold;
@@ -330,28 +320,11 @@ em {
font-weight: bold;
}
/* @adminobserver compatability*/
.adminobserver {
color: #996600;
font-weight: bold;
}
.admin {
color: #4473ff;
font-weight: bold;
}
/* cit-specific mentorsay*/
.mentorsay {
color: #E236D8;
font-weight: bold;
}
.mentorsay_admin {
color: #8A2BE2;
font-weight: bold;
}
.name {
font-weight: bold;
}
@@ -430,6 +403,14 @@ em {
color: #0000ff;
}
.greenteamradio {
color: #00ff00;
}
.yellowteamradio {
color: #d1ba22;
}
.yell {
font-weight: bold;
}
@@ -582,19 +563,6 @@ h1.alert, h2.alert {
color: #14a833;
}
.userlove {
color: #FF1493;
font-style: italic;
font-weight: bold;
text-shadow: 0 0 6px #ff6dbc;
}
.love {
color: #ff006a;
font-style: italic;
text-shadow: 0 0 6px #ff6d6d;
}
.cult {
color: #973e3b;
}
@@ -688,156 +656,12 @@ h1.alert, h2.alert {
font-size: 185%;
}
/* compatability */
.umbra {
color: #5000A0;
}
.umbra_emphasis {
color: #5000A0;
font-weight: bold;
font-style: italic;
}
.umbra_large {
color: #5000A0;
font-weight: bold;
font-style: italic;
font-size: 3;
}
.deconversion_message {
color: #5000A0;
font-size: 185%;
font-style: italic;
}
/* coggers dosen't exist on tg :( */
/* from decon_msg: font-size: 185% == font-size 3*/
.brass {
color: #BE8700;
}
.heavy_brass {
color: #BE8700;
font-weight: bold;
font-style: italic;
}
.large_brass {
color: #BE8700;
font-size: 185%;
}
.big_brass {
color: #BE8700;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.ratvar {
color: #BE8700;
font-size: 6;
font-weight: bold;
font-style: italic;
}
.alloy {
color: #42474D;
}
.heavy_alloy {
color: #42474D;
font-weight: bold;
font-style: italic;
}
.nezbere_large {
color: #42474D;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.nezbere {
color: #42474D;
font-weight: bold;
font-style: italic;
}
.nezbere_small {
color: #42474D;
}
.sevtug_large {
color: #AF0AAF;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.sevtug {
color: #AF0AAF;
font-weight: bold;
font-style: italic;
}
.sevtug_small {
color: #AF0AAF;
}
.inathneq_large {
color: #1E8CE1;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.inathneq {
color: #1E8CE1;
font-weight: bold;
font-style: italic;
}
.inathneq_small {
color: #1E8CE1;
}
.nzcrentr_large {
color: #DAAA18;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.nzcrentr {
color: #DAAA18;
font-weight: bold;
font-style: italic;
}
.nzcrentr_small {
color: #DAAA18;
}
.neovgre_large {
color: #6E001A;
font-size: 185%;
font-weight: bold;
font-style: italic;
}
.neovgre {
color: #6E001A;
font-weight: bold;
font-style: italic;
}
.neovgre_small {
color: #6E001A;
}
.ghostalert {
color: #5c00e6;
font-style: italic;
@@ -897,7 +721,7 @@ h1.alert, h2.alert {
.command_headset {
font-weight: bold;
font-size: 125%; /* 160% default, changed so command can scream less */
font-size: 160%;
}
.small {
@@ -926,14 +750,9 @@ h1.alert, h2.alert {
font-size: 185%;
}
.yellowtext {
color: #FFCC00;
font-size: 185%;
}
.clown {
color: #FF69Bf;
font-size: 125%;
font-size: 160%;
font-family: "Comic Sans MS", cursive, sans-serif;
font-weight: bold;
}
@@ -949,10 +768,6 @@ h1.alert, h2.alert {
font-style: italic;
}
.spooky { /* cit specific, used by jacq */
color: #FF9100;
}
.hypnophrase {
color: #0d0d0d;
font-weight: bold;
@@ -982,31 +797,6 @@ h1.alert, h2.alert {
}
}
.velvet { /* cit specific, used by MKU */
color: #660015;
font-weight: bold;
animation: velvet 5000ms infinite;
animation-direction: alternate;
}
@keyframes velvet {
0% {
color: #400020;
}
40% {
color: #FF0000;
}
50% {
color: #FF8888;
}
60% {
color: #FF0000;
}
100% {
color: #400020;
}
}
.phobia {
color: #dd0000;
font-weight: bold;
@@ -1032,6 +822,10 @@ h1.alert, h2.alert {
width: auto;
}
.bigicon {
font-size: 2.5em;
}
.memo {
color: #638500;
text-align: center;
+59 -120
View File
@@ -4,22 +4,11 @@
* @license MIT
*/
export const THEMES = ['light', 'dark', 'default'];
/*
export const THEMES = ['light', 'dark'];
const COLOR_DARK_BG = '#202020';
const COLOR_DARK_BG_DARKER = '#171717';
const COLOR_DARK_TEXT = '#a4bad6';
*/
/* CIT SPECIFIC DEFINES. */
const COLOR_DARK_INFO_BUTTONS_BG = "#40628A";
const COLOR_DARK_BG = "#272727";
const COLOR_DARK_DARKBG = "#242424";
const COLOR_DARK_TEXT = "#E0E0E0";
const COLOR_WHITE_INFO_BUTTONS_BG = "#90B3DD";
const COLOR_WHITE_BG = "#F0F0F0";
const COLOR_WHITE_DARKBG = "#E6E6E6";
const COLOR_WHITE_TEXT = "#000000";
let setClientThemeTimer = null;
@@ -45,57 +34,57 @@ export const setClientTheme = name => {
if (name === 'light') {
return Byond.winset({
// Main windows
'infowindow.background-color': COLOR_WHITE_DARKBG,
'infowindow.text-color': COLOR_WHITE_TEXT,
'info.background-color': COLOR_WHITE_BG,
'info.text-color': COLOR_WHITE_TEXT,
'browseroutput.background-color': COLOR_WHITE_DARKBG,
'browseroutput.text-color': COLOR_WHITE_TEXT,
'outputwindow.background-color': COLOR_WHITE_DARKBG,
'outputwindow.text-color': COLOR_WHITE_TEXT,
'mainwindow.background-color': COLOR_WHITE_DARKBG,
'split.background-color': COLOR_WHITE_BG,
'infowindow.background-color': 'none',
'infowindow.text-color': '#000000',
'info.background-color': 'none',
'info.text-color': '#000000',
'browseroutput.background-color': 'none',
'browseroutput.text-color': '#000000',
'outputwindow.background-color': 'none',
'outputwindow.text-color': '#000000',
'mainwindow.background-color': 'none',
'split.background-color': 'none',
// Buttons
'changelog.background-color': COLOR_WHITE_INFO_BUTTONS_BG,
'changelog.text-color': COLOR_WHITE_TEXT,
'rules.background-color': COLOR_WHITE_INFO_BUTTONS_BG,
'rules.text-color': COLOR_WHITE_TEXT,
'wiki.background-color': COLOR_WHITE_INFO_BUTTONS_BG,
'wiki.text-color': COLOR_WHITE_TEXT,
'forum.background-color': COLOR_WHITE_INFO_BUTTONS_BG,
'forum.text-color': COLOR_WHITE_TEXT,
'github.background-color': COLOR_WHITE_INFO_BUTTONS_BG,
'github.text-color': COLOR_WHITE_TEXT,
'report-issue.background-color': '#EF7F7F',
'report-issue.text-color': COLOR_WHITE_TEXT,
'changelog.background-color': 'none',
'changelog.text-color': '#000000',
'rules.background-color': 'none',
'rules.text-color': '#000000',
'wiki.background-color': 'none',
'wiki.text-color': '#000000',
'forum.background-color': 'none',
'forum.text-color': '#000000',
'github.background-color': 'none',
'github.text-color': '#000000',
'report-issue.background-color': 'none',
'report-issue.text-color': '#000000',
// Status and verb tabs
'output.background-color': COLOR_WHITE_BG,
'output.text-color': COLOR_WHITE_TEXT,
'statwindow.background-color': COLOR_WHITE_DARKBG,
'statwindow.text-color': COLOR_WHITE_TEXT,
'stat.background-color': COLOR_WHITE_BG,
'stat.tab-background-color': COLOR_WHITE_DARKBG,
'stat.text-color': COLOR_WHITE_TEXT,
'stat.tab-text-color': COLOR_WHITE_TEXT,
'stat.prefix-color': COLOR_WHITE_TEXT,
'stat.suffix-color': COLOR_WHITE_TEXT,
'output.background-color': 'none',
'output.text-color': '#000000',
'statwindow.background-color': 'none',
'statwindow.text-color': '#000000',
'stat.background-color': '#FFFFFF',
'stat.tab-background-color': 'none',
'stat.text-color': '#000000',
'stat.tab-text-color': '#000000',
'stat.prefix-color': '#000000',
'stat.suffix-color': '#000000',
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_WHITE_DARKBG,
'saybutton.text-color': COLOR_WHITE_TEXT,
// 'oocbutton.background-color': COLOR_WHITE_DARKBG,
// 'oocbutton.text-color': COLOR_WHITE_TEXT,
// 'mebutton.background-color': COLOR_WHITE_DARKBG,
// 'mebutton.text-color': COLOR_WHITE_TEXT,
'asset_cache_browser.background-color': COLOR_WHITE_DARKBG,
'asset_cache_browser.text-color': COLOR_WHITE_TEXT,
'tooltip.background-color': COLOR_WHITE_BG,
'tooltip.text-color': COLOR_WHITE_TEXT,
'saybutton.background-color': 'none',
'saybutton.text-color': '#000000',
'oocbutton.background-color': 'none',
'oocbutton.text-color': '#000000',
'mebutton.background-color': 'none',
'mebutton.text-color': '#000000',
'asset_cache_browser.background-color': 'none',
'asset_cache_browser.text-color': '#000000',
'tooltip.background-color': 'none',
'tooltip.text-color': '#000000',
});
}
if (name === 'dark') {
Byond.winset({
// Main windows
'infowindow.background-color': COLOR_DARK_DARKBG,
'infowindow.background-color': COLOR_DARK_BG,
'infowindow.text-color': COLOR_DARK_TEXT,
'info.background-color': COLOR_DARK_BG,
'info.text-color': COLOR_DARK_TEXT,
@@ -103,27 +92,27 @@ export const setClientTheme = name => {
'browseroutput.text-color': COLOR_DARK_TEXT,
'outputwindow.background-color': COLOR_DARK_BG,
'outputwindow.text-color': COLOR_DARK_TEXT,
'mainwindow.background-color': COLOR_DARK_DARKBG,
'mainwindow.background-color': COLOR_DARK_BG,
'split.background-color': COLOR_DARK_BG,
// Buttons
'changelog.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'changelog.background-color': '#494949',
'changelog.text-color': COLOR_DARK_TEXT,
'rules.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'rules.background-color': '#494949',
'rules.text-color': COLOR_DARK_TEXT,
'wiki.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'wiki.background-color': '#494949',
'wiki.text-color': COLOR_DARK_TEXT,
'forum.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'forum.background-color': '#494949',
'forum.text-color': COLOR_DARK_TEXT,
'github.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'github.background-color': '#3a3a3a',
'github.text-color': COLOR_DARK_TEXT,
'report-issue.background-color': '#A92C2C',
'report-issue.background-color': '#492020',
'report-issue.text-color': COLOR_DARK_TEXT,
// Status and verb tabs
'output.background-color': COLOR_DARK_BG,
'output.background-color': COLOR_DARK_BG_DARKER,
'output.text-color': COLOR_DARK_TEXT,
'statwindow.background-color': COLOR_DARK_DARKBG,
'statwindow.background-color': COLOR_DARK_BG_DARKER,
'statwindow.text-color': COLOR_DARK_TEXT,
'stat.background-color': COLOR_DARK_DARKBG,
'stat.background-color': COLOR_DARK_BG_DARKER,
'stat.tab-background-color': COLOR_DARK_BG,
'stat.text-color': COLOR_DARK_TEXT,
'stat.tab-text-color': COLOR_DARK_TEXT,
@@ -132,60 +121,10 @@ export const setClientTheme = name => {
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_DARK_BG,
'saybutton.text-color': COLOR_DARK_TEXT,
// 'oocbutton.background-color': COLOR_DARK_BG,
// 'oocbutton.text-color': COLOR_DARK_TEXT,
// 'mebutton.background-color': COLOR_DARK_BG,
// 'mebutton.text-color': COLOR_DARK_TEXT,
'asset_cache_browser.background-color': COLOR_DARK_BG,
'asset_cache_browser.text-color': COLOR_DARK_TEXT,
'tooltip.background-color': COLOR_DARK_BG,
'tooltip.text-color': COLOR_DARK_TEXT,
});
}
if (name === 'default') { // white-theme (chat) BUT game is using dorktheme
Byond.winset({
// Main windows
'infowindow.background-color': COLOR_DARK_DARKBG,
'infowindow.text-color': COLOR_DARK_TEXT,
'info.background-color': COLOR_DARK_BG,
'info.text-color': COLOR_DARK_TEXT,
'browseroutput.background-color': COLOR_DARK_BG,
'browseroutput.text-color': COLOR_DARK_TEXT,
'outputwindow.background-color': COLOR_DARK_BG,
'outputwindow.text-color': COLOR_DARK_TEXT,
'mainwindow.background-color': COLOR_DARK_DARKBG,
'split.background-color': COLOR_DARK_BG,
// Buttons
'changelog.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'changelog.text-color': COLOR_DARK_TEXT,
'rules.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'rules.text-color': COLOR_DARK_TEXT,
'wiki.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'wiki.text-color': COLOR_DARK_TEXT,
'forum.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'forum.text-color': COLOR_DARK_TEXT,
'github.background-color': COLOR_DARK_INFO_BUTTONS_BG,
'github.text-color': COLOR_DARK_TEXT,
'report-issue.background-color': '#A92C2C',
'report-issue.text-color': COLOR_DARK_TEXT,
// Status and verb tabs
'output.background-color': COLOR_DARK_BG,
'output.text-color': COLOR_DARK_TEXT,
'statwindow.background-color': COLOR_DARK_DARKBG,
'statwindow.text-color': COLOR_DARK_TEXT,
'stat.background-color': COLOR_DARK_DARKBG,
'stat.tab-background-color': COLOR_DARK_BG,
'stat.text-color': COLOR_DARK_TEXT,
'stat.tab-text-color': COLOR_DARK_TEXT,
'stat.prefix-color': COLOR_DARK_TEXT,
'stat.suffix-color': COLOR_DARK_TEXT,
// Say, OOC, me Buttons etc.
'saybutton.background-color': COLOR_DARK_BG,
'saybutton.text-color': COLOR_DARK_TEXT,
// 'oocbutton.background-color': COLOR_DARK_BG,
// 'oocbutton.text-color': COLOR_DARK_TEXT,
// 'mebutton.background-color': COLOR_DARK_BG,
// 'mebutton.text-color': COLOR_DARK_TEXT,
'oocbutton.background-color': COLOR_DARK_BG,
'oocbutton.text-color': COLOR_DARK_TEXT,
'mebutton.background-color': COLOR_DARK_BG,
'mebutton.text-color': COLOR_DARK_TEXT,
'asset_cache_browser.background-color': COLOR_DARK_BG,
'asset_cache_browser.text-color': COLOR_DARK_TEXT,
'tooltip.background-color': COLOR_DARK_BG,
+3 -3
View File
@@ -3,8 +3,8 @@
"name": "tgui-polyfill",
"version": "4.3.0",
"dependencies": {
"core-js": "^3.9.0",
"regenerator-runtime": "^0.13.7",
"whatwg-fetch": "^3.6.1"
"core-js": "^3.10.1",
"regenerator-runtime": "^0.13.8",
"whatwg-fetch": "^3.6.2"
}
}
+3 -1
View File
@@ -1,3 +1,5 @@
<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>
</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

After

Width:  |  Height:  |  Size: 1.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<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/ -->

After

Width:  |  Height:  |  Size: 1.2 KiB

+5 -5
View File
@@ -8,11 +8,11 @@ import { BooleanLike, classes, pureComponentHooks } from 'common/react';
import { Box, BoxProps, unit } from './Box';
export interface FlexProps extends BoxProps {
direction: string | BooleanLike;
wrap: string | BooleanLike;
align: string | BooleanLike;
justify: string | BooleanLike;
inline: BooleanLike;
direction?: string | BooleanLike;
wrap?: string | BooleanLike;
align?: string | BooleanLike;
justify?: string | BooleanLike;
inline?: BooleanLike;
}
export const computeFlexProps = (props: FlexProps) => {
+11 -5
View File
@@ -28,17 +28,23 @@ export const Icon = props => {
if (typeof rotation === 'number') {
style['transform'] = `rotate(${rotation}deg)`;
}
const faRegular = FA_OUTLINE_REGEX.test(name);
const faName = name.replace(FA_OUTLINE_REGEX, '');
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, '');
iconClass = (faRegular ? 'far ' : 'fas ') + 'fa-'+ faName + (spin ? " fa-spin" : "");
}
return (
<Box
as="i"
className={classes([
'Icon',
className,
faRegular ? 'far' : 'fas',
'fa-' + faName,
spin && 'fa-spin',
iconClass,
])}
style={style}
{...rest} />
@@ -10,7 +10,7 @@ import { Box, unit } from './Box';
import { Divider } from './Divider';
type LabeledListProps = {
children: InfernoNode;
children?: any;
};
export const LabeledList = (props: LabeledListProps) => {
+3 -9
View File
@@ -1,4 +1,4 @@
import { toFixed } from 'common/math';
import { formatTime } from '../format';
import { Component } from 'inferno';
// AnimatedNumber Copypaste
@@ -57,13 +57,7 @@ export class TimeDisplay extends Component {
if (!isSafeNumber(val)) {
return this.state.value || null;
}
// THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER
// HH:MM:SS
// 00:02:13
const seconds = toFixed(Math.floor((val/10) % 60)).padStart(2, "0");
const minutes = toFixed(Math.floor((val/(10*60)) % 60)).padStart(2, "0");
const hours = toFixed(Math.floor((val/(10*60*60)) % 24)).padStart(2, "0");
const formattedValue = `${hours}:${minutes}:${seconds}`;
return formattedValue;
return formatTime(val);
}
}
+10
View File
@@ -75,6 +75,16 @@ export const RADIO_CHANNELS = [
freq: 1217,
color: '#3434fd',
},
{
name: 'Green Team',
freq: 1219,
color: '#34fd34',
},
{
name: 'Yellow Team',
freq: 1221,
color: '#d1ba22',
},
{
name: 'CentCom',
freq: 1337,
+1 -1
View File
@@ -148,7 +148,7 @@ window.addEventListener('beforeunload', e => {
const keyHeldByCode = {};
class KeyEvent {
export class KeyEvent {
constructor(e, type, repeat) {
this.event = e;
this.type = type;
+27
View File
@@ -169,3 +169,30 @@ export const formatSiBaseTenUnit = (
);
return finalString.trim();
};
/**
* Formats decisecond count into HH::MM::SS display by default
* "short" format does not pad and adds hms suffixes
*/
export const formatTime = (val, formatType) => {
// THERE IS AS YET INSUFFICIENT DATA FOR A MEANINGFUL ANSWER
// HH:MM:SS
// 00:02:13
const seconds = toFixed(Math.floor((val/10) % 60));
const minutes = toFixed(Math.floor((val/(10*60)) % 60));
const hours = toFixed(Math.floor((val/(10*60*60)) % 24));
switch (formatType) {
case "short": {
const hours_truncated = hours > 0 ? `${hours}h` : "";
const minutes_truncated = minutes > 0 ? `${minutes}m` : "";
const seconds_truncated = seconds > 0 ? `${seconds}s` : "";
return `${hours_truncated}${minutes_truncated}${seconds_truncated}`;
}
default: {
const seconds_padded = seconds.padStart(2, "0");
const minutes_padded = minutes.padStart(2, "0");
const hours_padded = hours.padStart(2, "0");
return `${hours_padded}:${minutes_padded}:${seconds_padded}`;
}
}
};
+183
View File
@@ -0,0 +1,183 @@
/**
* @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> = {};
/**
* 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';
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.
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;
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) => {
handlePassthrough(key);
});
};
+1 -1
View File
@@ -8,7 +8,6 @@
import './styles/main.scss';
import './styles/themes/abductor.scss';
import './styles/themes/cardtable.scss';
import './styles/themes/clockcult.scss';
import './styles/themes/hackerman.scss';
import './styles/themes/malfunction.scss';
import './styles/themes/neutral.scss';
@@ -16,6 +15,7 @@ import './styles/themes/ntos.scss';
import './styles/themes/paper.scss';
import './styles/themes/retro.scss';
import './styles/themes/syndicate.scss';
import './styles/themes/wizard.scss';
import { perf } from 'common/perf';
import { setupHotReloading } from 'tgui-dev-server/link/client';
+69
View File
@@ -0,0 +1,69 @@
import { useBackend } from '../backend';
import { Button, Dropdown, Flex, Knob, LabeledControls, Section } from '../components';
import { Window } from '../layouts';
export const Aquarium = (props, context) => {
const { act, data } = useBackend(context);
const {
temperature,
fluid_type,
minTemperature,
maxTemperature,
fluidTypes,
contents,
allow_breeding,
} = data;
return (
<Window
width={500}
height={400}>
<Window.Content>
<Section title="Aquarium Controls">
<LabeledControls>
<LabeledControls.Item label="Temperature">
<Knob
size={1.25}
mb={1}
value={temperature}
unit="K"
minValue={minTemperature}
maxValue={maxTemperature}
step={1}
stepPixelSize={1}
onDrag={(e, value) => act('temperature', {
temperature: value,
})} />
</LabeledControls.Item>
<LabeledControls.Item label="Fluid">
<Flex direction="column" mb={1}>
{fluidTypes.map(f => (
<Flex.Item key={f}>
<Button
fluid
content={f}
selected={fluid_type === f}
onClick={() => act('fluid', { fluid: f })} />
</Flex.Item>
))}
</Flex>
</LabeledControls.Item>
<LabeledControls.Item label="Reproduction Prevention System">
<Button
content={allow_breeding ? "Offline" : "Online"}
selected={!allow_breeding}
onClick={() => act('allow_breeding')} />
</LabeledControls.Item>
</LabeledControls>
</Section>
<Section title="Contents">
{contents.map(movable => (
<Button
key={movable.ref}
content={movable.name}
onClick={() => act('remove', { ref: movable.ref })} />
))}
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,108 @@
import { useBackend, useSharedState } from '../backend';
import { Button, NoticeBox, Section, Table, Tabs } from '../components';
import { Window } from '../layouts';
import { ShuttleConsoleContent } from './ShuttleConsole';
export const AuxBaseConsole = (props, context) => {
const { data } = useBackend(context);
const [tab, setTab] = useSharedState(context, 'tab', 1);
const {
type,
blind_drop,
turrets = [],
} = data;
return (
<Window
width={turrets.length ? 620 : 350}
height={turrets.length ? 310 : 260}>
<Window.Content scrollable={!!turrets.length}>
<Tabs>
<Tabs.Tab
icon="list"
lineHeight="23px"
selected={tab === 1}
onClick={() => setTab(1)}>
{type === "shuttle" ? "Shuttle Launch" : "Base Launch"}
</Tabs.Tab>
<Tabs.Tab
icon="list"
lineHeight="23px"
selected={tab === 2}
onClick={() => setTab(2)}>
Turrets ({turrets.length})
</Tabs.Tab>
</Tabs>
{tab === 1 && (
<ShuttleConsoleContent
type={type}
blind_drop={blind_drop} />
)}
{tab === 2 && (
<AuxBaseConsoleContent />
)}
</Window.Content>
</Window>
);
};
const STATUS_COLOR_KEYS = {
"ERROR": "bad",
"Disabled": "bad",
"Firing": "average",
"All Clear": "good",
};
export const AuxBaseConsoleContent = (props, context) => {
const { act, data } = useBackend(context);
const {
turrets = [],
} = data;
return (
<Section
title={"Turret Control"}
buttons={(
!!turrets.length && (
<Button
icon="power-off"
content={"Toggle Power"}
onClick={() => act('turrets_power')} />
))}>
{!turrets.length ? (
<NoticeBox>
No connected turrets
</NoticeBox>
) : (
<Table cellpadding="3" textAlign="center">
<Table.Row header>
<Table.Cell>Unit</Table.Cell>
<Table.Cell>Condition</Table.Cell>
<Table.Cell>Status</Table.Cell>
<Table.Cell>Direction</Table.Cell>
<Table.Cell>Distance</Table.Cell>
<Table.Cell>Power</Table.Cell>
</Table.Row>
{turrets.map(turret => (
<Table.Row key={turret.key}>
<Table.Cell bold>{turret.name}</Table.Cell>
<Table.Cell>{turret.integrity}%</Table.Cell>
<Table.Cell
color={STATUS_COLOR_KEYS[turret.status] || "bad"}>
{turret.status}
</Table.Cell>
<Table.Cell>{turret.direction}</Table.Cell>
<Table.Cell>{turret.distance}m</Table.Cell>
<Table.Cell>
<Button
icon="power-off"
content="Toggle"
onClick={() => act('single_turret_power', {
single_turret_power: turret.ref,
})} />
</Table.Cell>
</Table.Row>
))}
</Table>
)}
</Section>
);
};
@@ -0,0 +1,116 @@
import { filter, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { toFixed } from 'common/math';
import { useBackend } from '../backend';
import { Button, Divider, LabeledList, NumberInput, ProgressBar, Section, Stack, Box, AnimatedNumber } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { Window } from '../layouts';
const mappedTopMargin = "2%";
export const BluespaceSender = (props, context) => {
const { act, data } = useBackend(context);
const {
on,
gas_transfer_rate,
price_multiplier,
} = data;
const bluespace_network_gases = flow([
filter(gas => gas.amount >= 0.01),
sortBy(gas => -gas.amount),
])(data.bluespace_network_gases || []);
const gasMax = Math.max(1, ...bluespace_network_gases.map(gas => gas.amount));
return (
<Window
title="Bluespace Sender"
width={500}
height={600}>
<Window.Content>
<Section
scrollable
fill
title="Bluespace Network Gases"
buttons={(
<>
<Button
mr={0.5}
color="transparent"
icon="info"
tooltipPosition="bottom-left"
tooltip={multiline`
Any gas you pipe into here will be added to the Bluespace
Network! That means any connected Bluespace Vendor (multitool)
will hook up to all the gas stored in this, and charge
this machine's price to buy it.
`} />
<NumberInput
animated
value={gas_transfer_rate}
step={0.01}
width="63px"
unit="moles/S"
minValue={0}
maxValue={1}
onDrag={(e, value) => act('rate', {
rate: value,
})} />
<Button
ml={0.5}
icon={data.on ? 'power-off' : 'times'}
content={data.on ? 'On' : 'Off'}
selected={data.on}
tooltipPosition="bottom-left"
tooltip="Will only take in gases while on."
onClick={() => act('power')} />
<Button
ml={0.5}
content="Retrieve gases"
tooltipPosition="bottom-left"
tooltip="Will transfer any gases inside to the pipe."
onClick={() => act('retrieve')} />
</>
)}>
<Box>
{"The vendors have made " + data.credits + " credits so far."}
</Box>
<Divider />
<LabeledList>
{bluespace_network_gases.map(gas => (
<>
<Stack key={gas.name}>
<Stack.Item color="label" basis={10} ml={1}>
{getGasLabel(gas.name) + " prices: "}
<br />
<Box mt={0.25}>
<NumberInput
animated
value={gas.price}
width="63px"
unit="per mole"
minValue={0}
maxValue={100}
onDrag={(e, value) => act('price', {
gas_price: value,
gas_type: gas.id,
})} />
</Box>
</Stack.Item>
<Stack.Item grow mt={mappedTopMargin} mr={1}>
<ProgressBar
color={getGasColor(gas.name)}
value={gas.amount}
minValue={0}
maxValue={gasMax}>
{toFixed(gas.amount, 2) + ' moles'}
</ProgressBar>
</Stack.Item>
</Stack>
<Divider />
</>
))}
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,147 @@
import { filter, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { toFixed } from 'common/math';
import { multiline } from 'common/string';
import { useBackend } from '../backend';
import { Button, Divider, LabeledList, NumberInput, ProgressBar, Section, Stack, Box } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { Window } from '../layouts';
export const BluespaceVendor = (props, context) => {
const { act, data } = useBackend(context);
const {
on,
tank_filling_amount,
price_multiplier,
pumping,
selected_gas,
} = data;
const bluespace_network_gases = flow([
filter(gas => gas.amount >= 0.01),
sortBy(gas => -gas.amount),
])(data.bluespace_network_gases || []);
const gasMax = Math.max(1, ...bluespace_network_gases.map(gas => gas.amount));
return (
<Window
title="Bluespace Vendor"
width={500}
height={600}>
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<Section
title="Controls"
buttons={(
<>
<Button
ml={1}
icon="plus"
content="Prepare Tank"
disabled={
data.pumping || data.inserted_tank || !data.tank_amount
}
onClick={() => act('tank_prepare')} />
<Button
ml={1}
icon="minus"
content="Remove Tank"
disabled={data.pumping || !data.inserted_tank}
onClick={() => act('tank_expel')} />
</>
)}>
<Stack>
<Stack.Item>
<NumberInput
animated
value={tank_filling_amount}
width="63px"
unit="% tank filling goal"
minValue={0}
maxValue={100}
onDrag={(e, value) => act('pumping_rate', {
rate: value,
})} />
</Stack.Item>
<Stack.Item grow>
{
<ProgressBar
value={data.tank_full / 1010}
ranges={{
good: [0.67, 1],
average: [0.34, 0.66],
bad: [0, 0.33],
}} />
}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section
scrollable
fill
title="Bluespace Network Gases"
buttons={(
<Button
color="transparent"
icon="info"
tooltipPosition="bottom-left"
tooltip={multiline`
Quick guide for machine use: prepare a tank to create a
new one in the machine, pick how much you want it filled,
and finally press start on the gas of your choice!
`} />
)}>
<LabeledList>
{bluespace_network_gases.map(gas => (
<>
<Stack key={gas.name}>
<Stack.Item color="label" basis={8} ml={1}>
{getGasLabel(gas.name) + " is " + gas.price + " credits per mole"}
</Stack.Item>
<Stack.Item grow mt={1}>
<ProgressBar
color={getGasColor(gas.name)}
value={gas.amount}
minValue={0}
maxValue={gasMax}>
{toFixed(gas.amount, 2) + ' moles'}
</ProgressBar>
</Stack.Item>
<Stack.Item ml={-0.1} mr={1} mt={1}>
{!data.pumping && data.selected_gas !== gas.id && (
<Button
ml={1}
icon="play"
tooltipPosition="left"
tooltip={"Start adding " + gas.name + "."}
disabled={data.pumping || !data.inserted_tank}
onClick={() => act('start_pumping', {
gas_id: gas.id,
})} />
) || (
<Button
ml={1}
disabled={data.selected_gas !== gas.id}
icon="minus"
tooltipPosition="left"
tooltip={"Stop adding " + gas.name + "."}
onClick={() => act('stop_pumping', {
gas_id: gas.id,
})} />
)}
</Stack.Item>
</Stack>
<Divider />
</>
))}
</LabeledList>
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,68 @@
import { useBackend } from '../backend';
import { AccessList } from './common/AccessList';
import { Window } from '../layouts';
export const ChameleonCard = (props, context) => {
const { act, data } = useBackend(context);
const {
accesses,
selectedList,
wildcardFlags,
wildcardSlots,
trimAccess,
accessFlags,
accessFlagNames,
showBasic,
ourAccess,
theftAccess,
ourTrimAccess,
} = data;
const parsedAccess = accesses.flatMap(region => {
const regionName = region.name;
const regionAccess = region.accesses;
const parsedRegion = {
name: regionName,
accesses: [],
};
parsedRegion.accesses = regionAccess.filter(access => {
// Snip everything that's part of our trim.
if (ourTrimAccess.includes(access.ref)) {
return false;
}
// Add anything not part of our trim that's an access (assumed wildcard)
// Also add any access on the ID card we're stealing from.
if (ourAccess.includes(access.ref) || theftAccess.includes(access.ref)) {
return true;
}
return false;
});
if (parsedRegion.accesses.length) {
return parsedRegion;
}
return [];
});
return (
<Window
width={500}
height={620}>
<Window.Content scrollable>
<AccessList
accesses={parsedAccess}
selectedList={selectedList}
wildcardFlags={wildcardFlags}
wildcardSlots={wildcardSlots}
trimAccess={trimAccess}
accessFlags={accessFlags}
accessFlagNames={accessFlagNames}
showBasic={!!showBasic}
accessMod={(ref, wildcard) => act('mod_access', {
access_target: ref,
access_wildcard: wildcard,
})} />
</Window.Content>
</Window>
);
};
@@ -0,0 +1,310 @@
import { round } from 'common/math';
import { useBackend } from '../backend';
import { AnimatedNumber, Box, Button, Flex, LabeledList, NumberInput, ProgressBar, RoundGauge, Section, Table } from '../components';
import { Window } from '../layouts';
import { BeakerContents } from './common/BeakerContents';
export const ChemRecipeDebug = (props, context) => {
const { act, data } = useBackend(context);
const {
targetTemp,
isActive,
isFlashing,
currentTemp,
currentpH,
forcepH,
forceTemp,
targetVol,
targatpH,
processing,
processAll,
index,
endIndex,
beakerSpawn,
minTemp,
editRecipeName,
editRecipeCold,
editRecipe = [],
chamberContents = [],
activeReactions = [],
queuedReactions = [],
} = data;
return (
<Window
width={450}
height={850}>
<Window.Content scrollable>
<Section
title="Controls"
buttons={(
<>
<Button
icon={beakerSpawn ? 'power-off' : 'times'}
selected={beakerSpawn}
content={"Spawn beaker"}
onClick={() => act('beakerSpawn')} />
<Button
icon={processAll ? 'power-off' : 'times'}
selected={processAll}
content={"All"}
onClick={() => act('all')} />
</>
)}>
<LabeledList>
<LabeledList.Item label="Reactions">
<Button
icon="plus"
onClick={() => act('setTargetList')} />
</LabeledList.Item>
<LabeledList.Item label="Queued">
{processAll && (
<Box>All</Box>
) || (
<Box>
{queuedReactions.length && (
queuedReactions.map(entry => (
entry.name+", "
))
)}
</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Temp">
{currentTemp}K
<NumberInput
width="65px"
unit="K"
step={10}
stepPixelSize={3}
value={round(targetTemp)}
minValue={0}
maxValue={1000}
onDrag={(e, value) => act('temperature', {
target: value,
})} />
<Button
icon={forceTemp ? 'power-off' : 'times'}
selected={forceTemp}
content={"Force"}
onClick={() => act('forceTemp')} />
<Button
icon={minTemp ? 'power-off' : 'times'}
selected={minTemp}
content={"MinTemp"}
onClick={() => act('minTemp')} />
</LabeledList.Item>
<LabeledList.Item label="Vol multi">
<NumberInput
width="65px"
unit="x"
step={1}
stepPixelSize={3}
value={round(targetVol)}
minValue={1}
maxValue={200}
onDrag={(e, value) => act('vol', {
target: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="pH">
{currentpH}
<NumberInput
width="65px"
step={0.1}
stepPixelSize={3}
value={targatpH}
minValue={0}
maxValue={14}
onDrag={(e, value) => act('pH', {
target: value,
})} />
<Button
icon={forcepH ? 'power-off' : 'times'}
selected={forcepH}
content={"Force"}
onClick={() => act('forcepH')} />
</LabeledList.Item>
<LabeledList.Item label="Index">
{index} of {endIndex}
</LabeledList.Item>
<LabeledList.Item label="Start">
<Button
icon={processing ? 'power-off' : 'times'}
selected={!!processing}
content={"Start"}
onClick={() => act('start')} />
<Button
icon={processing ? 'times' : 'power-off'}
color="red"
content={"Stop"}
onClick={() => act('stop')} />
</LabeledList.Item>
</LabeledList>
</Section>
<Section
title="Recipe edit">
<LabeledList>
<LabeledList.Item label={editRecipeName ? editRecipeName : "lookup"}>
<Button
icon={'flask'}
color="purple"
content={"Select recipe"}
onClick={() => act('setEdit')} />
</LabeledList.Item>
{!!editRecipe && (
<>
<LabeledList.Item label="is_cold_recipe">
<Button
icon={editRecipeCold ? 'smile' : 'times'}
color={editRecipeCold ? 'green' : 'red'}
content={"Cold?"}
onClick={() => act("updateVar", {
type: entry.name,
target: value,
})} />
</LabeledList.Item>
{editRecipe.map(entry => (
<LabeledList.Item label={entry.name} key={entry.name}>
<NumberInput
width="65px"
step={1}
stepPixelSize={3}
value={entry.var}
minValue={-9999}
maxValue={9999}
onDrag={(e, value) => act("updateVar", {
type: entry.name,
target: value,
})} />
</LabeledList.Item>
))}
</>
)}
<LabeledList.Item label="Export">
<Button
icon={'save'}
color="green"
content={"export"}
onClick={() => act('export')} />
</LabeledList.Item>
</LabeledList>
</Section>
<Section
title="Reactions"
buttons={(
<Flex>
<Flex.Item color="label">
<AnimatedNumber
value={currentpH}
format={value => 'pH: ' + round(value, 3)} />
</Flex.Item>
<Flex.Item>
<AnimatedNumber value={currentpH}>
{(_, value) => (
<RoundGauge
size={1.60}
value={value}
minValue={0}
maxValue={14}
alertAfter={isFlashing}
content={"test"}
format={value => null}
ranges={{
"red": [-0.22, 1.5],
"orange": [1.5, 3],
"yellow": [3, 4.5],
"olive": [4.5, 5],
"good": [5, 6],
"green": [6, 8.5],
"teal": [8.5, 9.5],
"blue": [9.5, 11],
"purple": [11, 12.5],
"violet": [12.5, 14],
}} />
)}
</AnimatedNumber>
</Flex.Item>
</Flex>
)}>
{activeReactions.length === 0 && (
<Box color="label">
No active reactions.
</Box>
) || (
<Table>
<Table.Row>
<Table.Cell bold color="label">
Reaction
</Table.Cell>
<Table.Cell bold color="label">
{"Reaction quality"}
</Table.Cell>
<Table.Cell bold color="label">
Target
</Table.Cell>
</Table.Row>
{activeReactions && activeReactions.map(reaction => (
<Table.Row key="reactions">
<Table.Cell width={'60px'} color={reaction.danger && "red"}>
{reaction.name}
</Table.Cell>
<Table.Cell width={'100px'} pr={'10px'}>
<AnimatedNumber value={reaction.quality}>
{(_, value) => (
<RoundGauge
size={1.30}
value={value}
minValue={0}
maxValue={1}
alertAfter={reaction.purityAlert}
content={"test"}
format={value => null}
ml={5}
ranges={{
"red": [0, reaction.minPure],
"orange": [reaction.minPure, reaction.inverse],
"yellow": [reaction.inverse, 0.8],
"green": [0.8, 1],
}} />
)}
</AnimatedNumber>
</Table.Cell>
<Table.Cell width={'70px'}>
<ProgressBar
value={reaction.reactedVol}
minValue={0}
maxValue={reaction.targetVol}
textAlign={'center'}
icon={reaction.overheat && "thermometer-full"}
width={7}
color={reaction.overheat ? "red" : "label"}>
{reaction.targetVol}u
</ProgressBar>
</Table.Cell>
</Table.Row>
))}
<Table.Row />
</Table>
)}
</Section>
<Section
title="Chamber"
buttons={(
<Box>
{isActive ? "Reacting" : "Waiting"}
</Box>
)} >
{chamberContents.length &&(
<BeakerContents
beakerLoaded
beakerContents={chamberContents} />
) || (
<Box>
Nothing
</Box>
)}
</Section>
</Window.Content>
</Window>
);
};
+107
View File
@@ -0,0 +1,107 @@
import { useBackend } from "../backend";
import {
Box,
Button,
Divider,
LabeledList,
Flex,
Section,
} from "../components";
import { Window } from "../layouts";
export const Clipboard = (props, context) => {
const { act, data } = useBackend(context);
const { pen, top_paper, top_paper_ref, paper, paper_ref } = data;
return (
<Window title="Clipboard" width={400} height={500}>
<Window.Content backgroundColor="#704D25" scrollable>
<Section>
{pen ? (
<LabeledList>
<LabeledList.Item
label="Pen"
buttons={
<Button icon="eject" onClick={() => act("remove_pen")} />
}
>
{pen}
</LabeledList.Item>
</LabeledList>
) : (
<Box color="white" align="center">
No pen attached!
</Box>
)}
</Section>
<Divider />
{top_paper ? (
<Flex
color="black"
backgroundColor="white"
style={{ padding: "2px 2px 0 2px" }}
>
<Flex.Item align="center" grow={1}>
<Box align="center">{top_paper}</Box>
</Flex.Item>
<Flex.Item>
<Button
icon={pen ? "pen" : "eye"}
onClick={() => act("edit_paper", { ref: top_paper_ref })}
/>
<Button
icon="tag"
onClick={() => act("rename_paper", { ref: top_paper_ref })}
/>
<Button
icon="eject"
onClick={() => act("remove_paper", { ref: top_paper_ref })}
/>
</Flex.Item>
</Flex>
) : (
<Section>
<Box color="white" align="center">
The clipboard is empty!
</Box>
</Section>
)}
{paper.length > 0 && <Divider />}
{paper.map((paper_item, index) => (
<Flex
key={paper_ref[index]}
color="black"
backgroundColor="white"
style={{ padding: "2px 2px 0 2px" }}
mb={0.5}
>
<Flex.Item>
<Button
icon="chevron-up"
color="transparent"
iconColor="black"
onClick={() => act("move_top_paper", { ref: paper_ref[index] })}
/>
</Flex.Item>
<Flex.Item align="center" grow={1}>
<Box align="center">{paper_item}</Box>
</Flex.Item>
<Flex.Item>
<Button
icon={pen ? "pen" : "eye"}
onClick={() => act("edit_paper", { ref: paper_ref[index] })}
/>
<Button
icon="tag"
onClick={() => act("rename_paper", { ref: paper_ref[index] })}
/>
<Button
icon="eject"
onClick={() => act("remove_paper", { ref: paper_ref[index] })}
/>
</Flex.Item>
</Flex>
))}
</Window.Content>
</Window>
);
};
@@ -0,0 +1,89 @@
import { useBackend } from '../backend';
import { Button, Dropdown, Input, Section, Stack, TextArea } from '../components';
import { Window } from '../layouts';
export const CommandReport = (props, context) => {
const { act, data } = useBackend(context);
const {
command_name,
custom_name,
command_name_presets = [],
command_report_content,
played_sound,
announcer_sounds = [],
announce_contents,
} = data;
return (
<Window
title="Create Command Report"
width={325}
height={525}>
<Window.Content>
<Stack vertical>
<Stack.Item>
<Section title="Set Central Command name:" textAlign="center">
<Dropdown
width="100%"
selected={command_name}
options={command_name_presets}
onSelected={value => act('update_command_name', {
updated_name: value,
})} />
{!!custom_name && (
<Input
width="100%"
mt={1}
value={command_name}
placeholder={command_name}
onChange={(e, value) => act("update_command_name", {
updated_name: value,
})} />
)}
</Section>
</Stack.Item>
<Stack.Item>
<Section title="Set announcement sound:" textAlign="center">
<Dropdown
width="100%"
displayText={played_sound}
options={announcer_sounds}
onSelected={value => act('set_report_sound', {
picked_sound: value,
})} />
</Section>
</Stack.Item>
<Stack.Item>
<Section title="Set report text:" textAlign="center">
<TextArea
height="200px"
mb={1}
value={command_report_content}
onChange={(e, value) => act("update_report_contents", {
updated_contents: value,
})} />
<Stack vertical>
<Stack.Item>
<Button.Checkbox
fluid
checked={announce_contents}
onClick={() => act("toggle_announce")}>
Announce Contents
</Button.Checkbox>
</Stack.Item>
<Stack.Item>
<Button.Confirm
fluid
icon="check"
color="good"
textAlign="center"
content="Submit Report"
onClick={() => act("submit_report")} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,97 @@
import { classes } from 'common/react';
import { useBackend } from "../backend";
import { Icon, Section, Table, Tooltip } from "../components";
import { Window } from "../layouts";
const commandJobs = [
"Head of Personnel",
"Head of Security",
"Chief Engineer",
"Research Director",
"Chief Medical Officer",
];
export const CrewManifest = (props, context) => {
const { data: { manifest, positions } } = useBackend(context);
return (
<Window title="Crew Manifest" width={350} height={500}>
<Window.Content scrollable>
{Object.entries(manifest).map(([dept, crew]) => (
<Section
className={"CrewManifest--" + dept}
key={dept}
title={
dept + (dept !== "Misc"
? ` (${positions[dept].open} positions open)` : "")
}
>
<Table>
{Object.entries(crew).map(([crewIndex, crewMember]) => (
<Table.Row key={crewIndex}>
<Table.Cell className={"CrewManifest__Cell"}>
{crewMember.name}
</Table.Cell>
<Table.Cell
className={classes([
"CrewManifest__Cell",
"CrewManifest__Icons",
])}
collapsing
>
{positions[dept].exceptions.includes(crewMember.rank) && (
<Icon className="CrewManifest__Icon" name="infinity">
<Tooltip
content="No position limit"
position="bottom"
/>
</Icon>
)}
{crewMember.rank === "Captain" && (
<Icon
className={classes([
"CrewManifest__Icon",
"CrewManifest__Icon--Command",
])}
name="star"
>
<Tooltip
content="Captain"
position="bottom"
/>
</Icon>
)}
{commandJobs.includes(crewMember.rank) && (
<Icon
className={classes([
"CrewManifest__Icon",
"CrewManifest__Icon--Command",
"CrewManifest__Icon--Chevron",
])}
name="chevron-up"
>
<Tooltip
content="Member of command"
position="bottom"
/>
</Icon>
)}
</Table.Cell>
<Table.Cell
className={classes([
"CrewManifest__Cell",
"CrewManifest__Cell--Rank",
])}
collapsing
>
{crewMember.rank}
</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
))}
</Window.Content>
</Window>
);
};
@@ -0,0 +1,95 @@
import { useBackend } from '../backend';
import { Stack, Button, Section, NoticeBox, LabeledList, Collapsible } from '../components';
import { Window } from '../layouts';
export const CryopodConsole = (props, context) => {
const { data } = useBackend(context);
const { account_name, allow_items } = data;
const welcomeTitle = `Hello, ${account_name || '[REDACTED]'}!`;
return (
<Window title="Cryopod Console" width={400} height={480}>
<Window.Content>
<Stack vertical>
<Section title={welcomeTitle}>
This automated cryogenic freezing unit will safely store your
corporeal form until your next assignment.
</Section>
<CrewList />
{!!allow_items && <ItemList />}
</Stack>
</Window.Content>
</Window>
);
};
const CrewList = (props, context) => {
const { data } = useBackend(context);
const { frozen_crew } = data;
return (
<Collapsible title="Stored Crew">
{!frozen_crew.length ? (
<NoticeBox>No stored crew!</NoticeBox>
) : (
<Section height={10} fill scrollable>
<LabeledList>
{frozen_crew.map((person) => (
<LabeledList.Item key={person} label={person.name}>
{person.job}
</LabeledList.Item>
))}
</LabeledList>
</Section>
)}
</Collapsible>
);
};
const ItemList = (props, context) => {
const { act, data } = useBackend(context);
const { frozen_items } = data;
const replaceItemName = (item) => {
let itemName = item.toString();
if (itemName.startsWith('the')) {
itemName = itemName.slice(4, itemName.length);
}
return itemName.replace(/^\w/, (c) => c.toUpperCase());
};
return (
<Collapsible title="Stored Items">
{!frozen_items.length ? (
<NoticeBox>No stored items!</NoticeBox>
) : (
<>
<Section height={12} fill scrollable>
<LabeledList>
{frozen_items.map((item, index) => (
<LabeledList.Item
key={item}
label={replaceItemName(item)}
buttons={
<Button
icon="arrow-down"
content="Drop"
mr={1}
onClick={() => act('one_item', { item: index + 1 })}
/>
}
/>
))}
</LabeledList>
</Section>
<Button
content="Drop All Items"
color="red"
onClick={() => act('all_items')}
/>
</>
)}
</Collapsible>
);
};
@@ -0,0 +1,113 @@
import { useBackend } from '../backend';
import { AnimatedNumber, Button, Flex, Input, LabeledList, ProgressBar, Section, Table, NumberInput, Box } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { toFixed } from 'common/math';
import { Window } from '../layouts';
const logScale = value => Math.log2(16 + Math.max(0, value)) - 4;
export const Crystallizer = (props, context) => {
const { act, data } = useBackend(context);
const selectedRecipes = data.selected_recipes || [];
const gasTypes = data.internal_gas_data || [];
const {
requirements,
internal_temperature,
progress_bar,
gas_input,
selected,
} = data;
return (
<Window
width={500}
height={600}>
<Window.Content scrollable>
<Section title="Controls">
<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="Recipe">
{selectedRecipes.map(recipe => (
<Button
key={recipe.id}
selected={recipe.id === selected}
content={recipe.name}
onClick={() => act('recipe', {
mode: recipe.id,
})} />
))}
</LabeledList.Item>
<LabeledList.Item label="Gas Input">
<NumberInput
animated
value={parseFloat(data.gas_input)}
width="63px"
unit="moles/s"
minValue={0}
maxValue={250}
onDrag={(e, value) => act('gas_input', {
gas_input: value,
})} />
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Requirements and progress">
<LabeledList>
<LabeledList.Item label="Progress">
<ProgressBar
value={progress_bar / 100}
ranges={{
good: [0.67, 1],
average: [0.34, 0.66],
bad: [0, 0.33],
}} />
</LabeledList.Item>
<LabeledList.Item label="Recipe">
<Box m={1} style={{
'white-space': 'pre-wrap',
}}>
{requirements}
</Box>
</LabeledList.Item>
<LabeledList.Item label="Temperature">
<ProgressBar
value={logScale(internal_temperature)}
minValue={0}
maxValue={logScale(10000)}
ranges={{
teal: [-Infinity, logScale(80)],
good: [logScale(80), logScale(600)],
average: [logScale(600), logScale(5000)],
bad: [logScale(5000), Infinity],
}}>
{toFixed(internal_temperature) + ' K'}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Gases">
<LabeledList>
{gasTypes.map(gas => (
<LabeledList.Item
key={gas.name}
label={getGasLabel(gas.name)}>
<ProgressBar
color={getGasColor(gas.name)}
value={gas.amount}
minValue={0}
maxValue={1000}>
{toFixed(gas.amount, 2) + ' moles'}
</ProgressBar>
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,857 @@
import { useBackend, useLocalState } from '../backend';
import { BlockQuote, Box, Button, Dimmer, Icon, LabeledList, Modal, ProgressBar, Section, Stack } from '../components';
import { Window } from '../layouts';
import { resolveAsset } from '../assets';
import { formatTime } from '../format';
import { capitalize } from 'common/string';
import nt_logo from '../assets/bg-nanotrasen.svg';
import { Fragment } from 'inferno';
type ExplorationEventData = {
name: string,
ref: string
}
type FullEventData = {
image: string,
description: string,
action_enabled: boolean,
action_text: string,
skippable: boolean,
ignore_text: string,
ref: string
}
type ChoiceData = {
key: string,
text: string
}
type AdventureData = {
description: string,
image: string,
raw_image: string,
choices: Array<ChoiceData>
}
type SiteData = {
name: string,
ref: string,
description: string,
coordinates: string,
distance: number,
band_info: Record<string, number>,
revealed: boolean,
point_scan_complete: boolean,
deep_scan_complete: boolean,
events: Array<ExplorationEventData>
}
enum DroneStatusEnum {
Idle = "idle",
Travel = "travel",
Exploration = "exploration",
Adventure = "adventure",
Busy = "busy"
}
enum CargoType {
Tool = "tool",
Cargo = "cargo",
Empty = "empty"
}
type CargoData = {
type: CargoType,
name: string
}
type DroneBasicData = {
name: string,
description: string,
controlled: boolean,
ref: string,
}
type ExodroneConsoleData = {
signal_lost: boolean,
drone: boolean,
all_drones?: Array<DroneBasicData>
drone_status?: DroneStatusEnum,
drone_name?: string,
drone_integrity?: number,
drone_max_integrity?: number,
drone_travel_coefficent?: number,
drone_log?: Array<string>,
configurable?: boolean,
cargo?: Array<CargoData>,
can_travel?: boolean,
travel_error: string,
sites?: Array<SiteData>,
site?: SiteData,
travel_time?: number,
travel_time_left?: number,
wait_time_left?: number,
wait_message?: string,
event?: FullEventData,
adventure_data?: AdventureData,
// ui_static_data
all_tools: Record<string, ToolData>,
all_bands: Record<string, string>
}
type ToolData = {
description: string,
icon: string
}
export const ExodroneConsole = (props, context) => {
const { data } = useBackend<ExodroneConsoleData>(context);
const {
signal_lost,
} = data;
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
return (
<Window width={650} height={500}>
{!!signal_lost && <SignalLostModal />}
{!!choosingTools && <ToolSelectionModal />}
<Window.Content>
<ExodroneConsoleContent />
</Window.Content>
</Window>
);
};
const SignalLostModal = (props, context) => {
const { act } = useBackend(context);
return (
<Modal
backgroundColor="red"
textAlign="center"
width={30}
height={22}
p={0}
style={{ "border-radius": "5%" }}>
<img src={nt_logo} width={64} height={64} />
<Box
backgroundColor="black"
textColor="red"
fontSize={2}
style={{ "border-radius": "-10%" }}>
CONNECTION LOST
</Box>
<Box p={2} italic>
Connection to exploration drone interrupted.
Please contact nearest Nanotrasen Exploration Division
representative for further instructions.
</Box>
<Icon
name="exclamation-triangle"
textColor="black"
size={5} />
<Box>
<Button
content="Confirm"
color="danger"
style={{ "border": "1px solid black" }}
onClick={() => act("confirm_signal_lost")} />
</Box>
</Modal>
);
};
const DroneSelectionSection = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
all_drones,
} = data;
return (
<Section scrollable fill title="Exploration Drone Listing">
<Stack vertical>
{all_drones.map(drone => (
<Fragment key={drone.ref}>
<Stack.Item grow>
<Stack fill>
<Stack.Item basis={10} fontFamily="monospace" fontSize="18px">
{drone.name}
</Stack.Item>
<Stack.Divider />
<Stack.Item fontFamily="monospace" mt={0.8}>
{drone.description}
</Stack.Item>
<Stack.Item grow />
<Stack.Divider mr={1} />
<Stack.Item ml={0}>
{drone.controlled && (
"Controlled by another console."
) || (
<Button
content="Assume Control"
icon="plug"
onClick={() => act("select_drone", { "drone_ref": drone.ref })} />
)}
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Divider />
</Fragment>
))}
</Stack>
</Section>
);
};
const ToolSelectionModal = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
all_tools = {},
} = data;
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
const toolData = Object.keys(all_tools);
return (
<Modal>
<Stack fill vertical pr={2}>
<Stack.Item>
Select Tool:
</Stack.Item>
<Stack.Item>
<Stack textAlign="center">
{!!toolData && toolData.map(tool_name => (
<Stack.Item key={tool_name}>
<Button
onClick={() => {
setChoosingTools(false);
act("add_tool", { tool_type: tool_name });
}}
width={6}
height={6}
tooltip={all_tools[tool_name].description}>
<Stack vertical>
<Stack.Item>
{capitalize(tool_name)}
</Stack.Item>
<Stack.Item ml={2.5}>
<Icon name={all_tools[tool_name].icon} size={3} />
</Stack.Item>
</Stack>
</Button>
</Stack.Item>
)) || (
<Stack.Item>
<Button
content="Back" />
</Stack.Item>
)}
</Stack>
</Stack.Item>
</Stack>
</Modal>
);
};
const EquipmentBox = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
configurable,
all_tools = {},
} = data;
const cargo = props.cargo;
const boxContents = cargo => {
switch (cargo.type) {
case "tool": // Tool icon+Remove button if configurable
return (
<Stack direction="column">
<Stack.Item grow>
<Button
height={4.7}
width={4.7}
tooltip={capitalize(cargo.name)}
tooltipPosition="right"
color="transparent">
<Icon
color="white"
name={all_tools[cargo.name].icon}
size={3}
pl={1.5}
pt={2} />
</Button>
</Stack.Item>
{!!configurable && (
<Stack.Item mt={-9.4} textAlign="right">
<Button
onClick={() => act("remove_tool", { tool_type: cargo.name })}
color="danger"
icon="minus"
tooltipPosition="right"
tooltip="Remove Tool" />
</Stack.Item>
)}
</Stack>
);
case "cargo":// Jettison button.
return (
<Stack direction="column">
<Stack.Item>
<Button
mt={0}
height={4.7}
width={4.7}
tooltip={capitalize(cargo.name)}
tooltipPosition="right"
color="transparent">
<Icon
color="white"
name="box"
size={3}
pl={2.2}
pt={2} />
</Button>
</Stack.Item>
<Stack.Item mt={-9.4} textAlign="right">
<Button
onClick={() => act("jettison", { target_ref: cargo.ref })}
color="danger"
icon="minus"
tooltipPosition="right"
tooltip={`Jettison ${cargo.name}`} />
</Stack.Item>
</Stack>
);
case "empty":
return "";
}
};
return (
<Box
width={5}
height={5}
style={{ border: '2px solid black' }}
textAlign="center">
{boxContents(cargo)}
</Box>
);
};
const EquipmentGrid = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
cargo,
configurable,
} = data;
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
return (
<Stack vertical fill>
<Stack.Item grow>
<Section fill title="Controls">
<Stack vertical textAlign="center">
<Stack.Item>
<Button
fluid
icon="plug"
content="Disconnect"
onClick={() => act('end_control')} />
</Stack.Item>
<Stack.Divider />
<Stack.Item>
<Button.Confirm
fluid
icon="bomb"
content="Self-Destruct"
color="bad"
onClick={() => act('self_destruct')} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<Section title="Cargo">
<Stack.Item>
{!!configurable && (
<Button
fluid
color="average"
icon="wrench"
content="Install Tool"
onClick={() => setChoosingTools(true)} />
)}
</Stack.Item>
<Stack.Item>
<Stack wrap="wrap" width={10}>
{cargo.map(cargo_element => (
<EquipmentBox
key={cargo_element.name}
cargo={cargo_element} />
))}
</Stack>
</Stack.Item>
</Section>
</Stack.Item>
</Stack>
);
};
const DroneStatus = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone_integrity,
drone_max_integrity,
} = data;
return (
<Stack ml={-40}>
<Stack.Item color="label" mt={0.2}>
Integrity:
</Stack.Item>
<Stack.Item grow>
<ProgressBar
width="200px"
ranges={{
good: [0.7 * drone_max_integrity, drone_max_integrity],
average: [0.4 * drone_max_integrity, 0.7 * drone_max_integrity],
bad: [-Infinity, 0.4 * drone_max_integrity],
}}
value={drone_integrity}
maxValue={drone_max_integrity} />
</Stack.Item>
</Stack>
);
};
const NoSiteDimmer = () => {
return (
<Dimmer>
<Stack textAlign="center" vertical>
<Stack.Item>
<Icon
color="red"
name="map"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="red">
No Destinations.
</Stack.Item>
<Stack.Item basis={0} color="red">
(Use the Scanner Array Console to find new locations.)
</Stack.Item>
</Stack>
</Dimmer>
);
};
const TravelTargetSelectionScreen = (props, context) => {
// List of sites and eta travel times to each
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
sites,
site,
can_travel,
travel_error,
drone_travel_coefficent,
all_bands,
drone_status,
} = data;
const travel_cost = target_site => {
if (site) {
return Math.max(Math.abs(site.distance - target_site.distance), 1)
* drone_travel_coefficent;
}
else {
return target_site.distance * drone_travel_coefficent;
}
};
const [
choosingTools,
setChoosingTools,
] = useLocalState(context, 'choosingTools', false);
const [
TravelDimmerShown,
setTravelDimmerShown,
] = useLocalState(context, 'TravelDimmerShown', false);
const travel_to = ref => {
setTravelDimmerShown(false);
act("start_travel", { "target_site": ref });
};
const non_empty_bands = (dest : SiteData) => {
const band_check = (s: string) => dest.band_info[s] !== undefined
&& dest.band_info[s] !== 0;
return Object.keys(all_bands).filter(band_check);
};
const valid_destinations = !!sites && sites.filter(destination => (
!site || destination.ref !== site.ref
));
return (
drone_status === "travel" && (
<TravelDimmer />
) || (
<Section
title="Travel Destinations"
fill
scrollable
buttons={
<>
{props.showCancelButton && (
<Button
ml={5}
mr={0}
content="Cancel"
onClick={() => setTravelDimmerShown(false)} />
)}
<Box mt={props.showCancelButton && -3.5}>
<DroneStatus />
</Box>
</>
}>
{((sites && !sites.length) && !choosingTools) && (
<NoSiteDimmer />
)}
{site && (
<Section
mt={1}
title="Home"
buttons={
<Box>
ETA: {formatTime(site.distance * drone_travel_coefficent, "short")}
<Button
ml={1}
content={can_travel ? "Launch!" : travel_error}
onClick={() => travel_to(null)}
disabled={!can_travel} />
</Box>
}
/>
)}
{valid_destinations.map(destination => (
<Section
key={destination.ref}
title={destination.name}
buttons={
<>
ETA: {formatTime(travel_cost(destination), "short")}
<Button
ml={1}
content={can_travel ? "Launch!" : travel_error}
onClick={() => travel_to(destination.ref)}
disabled={!can_travel} />
</>
}>
<LabeledList>
<LabeledList.Item label="Location">
{destination.coordinates}
</LabeledList.Item>
<LabeledList.Item label="Description">
{destination.description}
</LabeledList.Item>
<LabeledList.Divider />
{non_empty_bands(destination).map(band => (
<LabeledList.Item
key={band}
label={band}>
{destination.band_info[band]}
</LabeledList.Item>
))}
</LabeledList>
</Section>
))}
</Section>
)
);
};
const TravelDimmer = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
travel_time,
travel_time_left,
} = data;
return (
<Section fill>
<Dimmer>
<Stack textAlign="center" vertical>
<Stack.Item>
<Icon
color="yellow"
name="route"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="yellow">
Travel Time: {formatTime(travel_time_left)}
</Stack.Item>
</Stack>
</Dimmer>
</Section>
);
};
const TimeoutScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
wait_time_left,
wait_message,
} = data;
return (
<Section fill>
<Dimmer>
<Stack textAlign="center" vertical>
<Stack.Item>
<Icon
color="green"
name="cog"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="green">
{wait_message} ({formatTime(wait_time_left)})
</Stack.Item>
</Stack>
</Dimmer>
</Section>
);
};
const ExplorationScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
site,
event,
sites,
} = data;
const [
TravelDimmerShown,
setTravelDimmerShown,
] = useLocalState(context, 'TravelDimmerShown', false);
if (TravelDimmerShown) {
return (<TravelTargetSelectionScreen showCancelButton />);
}
return (
<Section
fill
title="Exploration"
buttons={
<DroneStatus />
}>
<Stack vertical fill>
<Stack.Item grow>
<LabeledList>
<LabeledList.Item label="Site">{site.name}</LabeledList.Item>
<LabeledList.Item label="Location">{site.coordinates}</LabeledList.Item>
<LabeledList.Item label="Description">{site.description}</LabeledList.Item>
</LabeledList>
</Stack.Item>
<Stack.Item align="center" grow>
<Button
content="Explore!"
onClick={() => act("explore")} />
</Stack.Item>
{site.events.map(e => (
<Stack.Item
align="center"
key={site.ref}
grow>
<Button
content={capitalize(e.name)}
onClick={() => act("explore_event", { target_event: e.ref })} />
</Stack.Item>))}
<Stack.Item align="center" grow>
<Button
content="Travel"
onClick={() => setTravelDimmerShown(true)} />
</Stack.Item>
</Stack>
</Section>
);
};
const EventScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone_status,
event,
} = data;
return (
<Section
fill
title="Exploration"
buttons={
<DroneStatus />
}>
{(drone_status && drone_status === "busy") && (
<TimeoutScreen />
)}
<Stack vertical fill textAlign="center">
<Stack.Item>
<Stack fill>
<Stack.Item>
<img src={resolveAsset(event.image)}
height="125px"
width="250px"
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
</Stack.Item>
<Stack.Item >
<BlockQuote
style={{ "white-space": "pre-wrap" }}>
{event.description}
</BlockQuote>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow>
<Stack vertical fill >
<Stack.Item grow />
<Stack.Item grow>
<Button
content={event.action_text}
disabled={!event.action_enabled}
onClick={() => act("start_event")} />
</Stack.Item>
{!!event.skippable && (
<Stack.Item mt={2}>
<Button
content={event.ignore_text}
onClick={() => act("skip_event")} />
</Stack.Item>
)}
<Stack.Item grow />
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const AdventureScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
adventure_data,
} = data;
const rawData = adventure_data.raw_image;
const imgSource = rawData ? rawData : resolveAsset(adventure_data.image);
return (
<Section
fill
title="Exploration"
buttons={<DroneStatus />}>
<Stack>
<Stack.Item>
<BlockQuote style={{ "white-space": "pre-wrap" }}>{adventure_data.description}</BlockQuote>
</Stack.Item>
<Stack.Divider />
<Stack.Item>
<img
src={imgSource}
height="100px"
width="200px"
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
<Stack vertical>
<Stack.Divider />
<Stack.Item grow />
{!!adventure_data.choices && adventure_data.choices.map(choice => (
<Stack.Item key={choice.key}>
<Button
fluid
content={choice.text}
textAlign="center"
onClick={() => act('adventure_choice', { choice: choice.key })} />
</Stack.Item>
))}
<Stack.Item grow />
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const DroneScreen = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone_status,
event,
} = data;
switch (drone_status) {
case "busy":
return <TimeoutScreen />;
case "idle":
case "travel":
return <TravelTargetSelectionScreen />;
case "adventure":
return <AdventureScreen />;
case "exploration":
if (event) {
return <EventScreen />;
}
else {
return <ExplorationScreen />;
}
}
};
const ExodroneConsoleContent = (props, context) => {
const { act, data } = useBackend<ExodroneConsoleData>(context);
const {
drone,
drone_name,
drone_log,
} = data;
if (!drone) {
return <DroneSelectionSection />;
}
return (
<Stack fill vertical>
<Stack.Item grow>
<Stack vertical fill grow={2}>
<Stack.Item grow>
<Stack fill>
<Stack.Item>
<EquipmentGrid />
</Stack.Item>
<Stack.Item grow basis={0}>
<DroneScreen />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item height={10}>
<Section title="Drone Log" fill scrollable>
<LabeledList>
{drone_log.map((log_line, ix) => (
<LabeledList.Item key={log_line} label={`Entry ${ix + 1}`}>
{log_line}
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Stack.Item>
</Stack>
);
};
@@ -0,0 +1,298 @@
import { useBackend } from '../backend';
import { BlockQuote, Box, Button, Flex, Icon, Modal, Section, LabeledList, NoticeBox, Stack } from '../components';
import { Window } from '../layouts';
import { formatTime } from '../format';
type SiteData = {
name: string,
ref: string,
description: string,
distance: number,
band_info: Record<string, string>,
revealed: boolean,
}
type ScanData = {
scan_power: number,
point_scan_eta: number,
deep_scan_eta: number,
point_scan_complete: boolean,
deep_scan_complete: boolean
site_data: SiteData
}
const ScanFailedModal = (props, context) => {
const { act, data } = useBackend(context);
return (
<Modal>
<Flex direction="column">
<Flex.Item>
<Box color="bad">SCAN FAILURE!</Box>
</Flex.Item>
<Flex.Item>
<Button
content="Confirm"
onClick={() => act("confirm_fail")} />
</Flex.Item>
</Flex>
</Modal>);
};
const ScanSelectionSection = (props, context) => {
const { act, data } = useBackend<ScanData>(context);
const {
scan_power,
point_scan_eta,
deep_scan_eta,
point_scan_complete,
deep_scan_complete,
site_data,
} = data;
const site = site_data;
const point_cost = scan_power > 0 ? formatTime(point_scan_eta, "short") : "∞";
const deep_cost = scan_power > 0 ? formatTime(deep_scan_eta, "short") : "∞";
const scan_availible = !point_scan_complete || !deep_scan_complete;
return (
<Stack vertical fill>
<Stack.Item grow>
<Section
fill
title="Site Data"
buttons={
<Button
content="Back"
onClick={() => act("select_site", { "site_ref": null })} />
}>
<LabeledList>
<LabeledList.Item label="Name">{site.name}</LabeledList.Item>
<LabeledList.Item label="Description">
{site.revealed ? site.description : "No Data"}
</LabeledList.Item>
<LabeledList.Item label="Distance">{site.distance}</LabeledList.Item>
<LabeledList.Divider />
<LabeledList.Item label="Spectrography Data" />
<LabeledList.Divider />
{Object.keys(site.band_info).map(band => (
<LabeledList.Item
key={band}
label={band}>
{site.band_info[band]}
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Stack.Item>
{scan_availible && (
<Stack.Item>
<Section fill title="Scans">
{!point_scan_complete && (
<Section title="Point Scan">
<BlockQuote>
Point scan performs rudimentary scan of
the site, revealing its general characteristics.
</BlockQuote>
<Box>
<Button
content="Scan"
disabled={scan_power <= 0}
onClick={() => act("start_point_scan")} />
<Box
inline
pl={3}>
Estimated Time: {point_cost}.
</Box>
</Box>
</Section>
)}
{!deep_scan_complete && (
<Section title="Deep Scan">
<BlockQuote>
Deep scan performs full scan
of the site, revealing all details.
</BlockQuote>
<Box>
<Button
content="Scan"
disabled={scan_power <= 0}
onClick={() => act("start_deep_scan")} />
<Box
inline
pl={3}>
Estimated Time: {deep_cost}.
</Box>
</Box>
</Section>
)}
</Section>
</Stack.Item>
)}
</Stack>
);
};
type ScanInProgressData = {
scan_time: number,
scan_power: number,
scan_description: string,
}
const ScanInProgressModal = (props, context) => {
const { act, data } = useBackend<ScanInProgressData>(context);
const {
scan_time,
scan_power,
scan_description,
} = data;
return (
<Modal ml={1}>
<NoticeBox>Scan in Progress!</NoticeBox>
<Box color="danger" />
<LabeledList>
<LabeledList.Item
label="Scan summary">
{scan_description}
</LabeledList.Item>
<LabeledList.Item
label="Time left">
{formatTime(scan_time)}
</LabeledList.Item>
<LabeledList.Item
label="Scanning array power">
{scan_power}
</LabeledList.Item>
<LabeledList.Item
label="Emergency Stop">
<Button.Confirm
content="STOP SCAN"
color="red"
icon="times"
onClick={() => act("stop_scan")} />
</LabeledList.Item>
</LabeledList>
</Modal>
);
};
type ExoscannerConsoleData = {
scan_in_progress: boolean,
scan_power: number,
possible_sites: Array<SiteData>,
wide_scan_eta: number,
selected_site: string,
failed: boolean,
scan_conditions: Array<string>,
}
export const ExoscannerConsole = (props, context) => {
const { act, data } = useBackend<ExoscannerConsoleData>(context);
const {
scan_in_progress,
scan_power,
possible_sites = [],
wide_scan_eta,
selected_site,
failed,
scan_conditions = [],
} = data;
const can_start_wide_scan = scan_power > 0;
return (
<Window>
{!!scan_in_progress && (
<ScanInProgressModal />
)}
{!!failed && (
<ScanFailedModal />
)}
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<Section fill title="Available array power">
<Stack>
<Stack.Item grow>
{scan_power > 0 && (
<>
<Box pr={1} inline fontSize={2}>{scan_power}</Box>
<Icon
name="satellite-dish"
size={3} />
</>
) || (
"No properly configured scanner arrays detected."
)}
</Stack.Item>
</Stack>
<Section title="Special Scan Condtions">
{scan_conditions && scan_conditions.map(condition => (
<NoticeBox
key={condition}
warning>
{condition}
</NoticeBox>
))}
</Section>
</Section>
</Stack.Item>
{!!selected_site && (
<Stack.Item grow>
<ScanSelectionSection site_ref={selected_site} />
</Stack.Item>
)}
{!selected_site && (
<>
<Stack.Item>
<Section fill title="Configure Wide Scan">
<Stack>
<Stack.Item>
<BlockQuote>
Broad spectrum scan looking for
anything not matching known start charts.
</BlockQuote>
</Stack.Item>
<Stack.Item>
Cost estimate: {scan_power > 0 ? formatTime(wide_scan_eta, "short") : "∞ minutes"}
</Stack.Item>
<Stack.Item>
<Button
mt={2}
content="Scan"
disabled={!can_start_wide_scan}
onClick={() => act("start_wide_scan")} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section
fill
title="Configure Targeted Scans"
scrollable
buttons={
<Button
content="View Experiments"
onClick={() => act("open_experiments")}
icon="tasks" />
}>
<Stack vertical>
{possible_sites.map(site => (
<Stack.Item key={site.ref}>
<Button
content={site.name}
onClick={() => act("select_site", { "site_ref": site.ref })} />
</Stack.Item>
))}
</Stack>
</Section>
</Stack.Item>
</>
)}
</Stack>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,237 @@
import { Window } from '../layouts';
import { useBackend } from '../backend';
import { Section, Box, Button, Flex, Icon, LabeledList, Table, Tooltip } from '../components';
import { sortBy } from 'common/collections';
const ExperimentStages = props => {
return (
<Table ml={2} className="ExperimentStage__Table">
{props.children.map((stage, idx) =>
(<ExperimentStageRow key={idx} {...stage} />))}
</Table>
);
};
const ExperimentStageRow = props => {
const [type, description, value, altValue] = props;
// Determine completion based on type of stage
let completion = false;
switch (type) {
case "bool":
case "detail":
completion = value;
break;
case "integer":
completion = value === altValue;
break;
case "float":
completion = value >= 1;
break;
}
return (
<Table.Row
className={`ExperimentStage__StageContainer
${completion ? "complete" : "incomplete"}`}>
<Table.Cell
collapsing
className={`ExperimentStage__Indicator ${type}`}
color={completion ? "good" : "bad"}>
{(type === "bool" && <Icon name={value ? "check" : "times"} />)
|| (type === "integer" && `${value}/${altValue}`)
|| (type === "float" && `${value * 100}%`)
|| (type === "detail" && "⤷")}
</Table.Cell>
<Table.Cell className="ExperimentStage__Description">
{description}
</Table.Cell>
</Table.Row>
);
};
export const TechwebServer = (props, context) => {
const { act, data } = useBackend(context);
const { servers } = props;
return (
<Box m={1} className="ExperimentTechwebServer__Web">
<Flex align="center" justify="space-between"
className="ExperimentTechwebServer__WebHeader">
<Flex.Item className="ExperimentTechwebServer__WebName">
{servers[0].web_id} / {servers[0].web_org}
</Flex.Item>
<Flex.Item>
<Button
onClick={() => servers[0].selected
? act("clear_server")
: act("select_server", { "ref": servers[0].ref })}
content={servers[0].selected ? "Disconnect" : "Connect"}
backgroundColor={servers[0].selected ? "good" : "rgba(0, 0, 0, 0.4)"}
className="ExperimentTechwebServer__ConnectButton" />
</Flex.Item>
</Flex>
<Box className="ExperimentTechwebServer__WebContent">
<span>
Connectivity to this web is maintained by the following servers...
</span>
<LabeledList>
{servers.map((server, index) => {
return (
<LabeledList.Item
key={index}
label={server.name}>
<i>Located in {server.location}</i>
</LabeledList.Item>
);
})}
</LabeledList>
</Box>
</Box>
);
};
export const ExperimentConfigure = (props, context) => {
const { act, data } = useBackend(context);
const { always_active, has_start_callback } = data;
let servers = data.servers ?? [];
const experiments = sortBy(
exp => exp.name
)(data.experiments ?? []);
// Group servers together by web
let webs = new Map();
servers.forEach(x => {
if (x.web_id !== null) {
if (!webs.has(x.web_id)) {
webs.set(x.web_id, []);
}
webs.get(x.web_id).push(x);
}
});
return (
<Window
resizable
width={600}
height={735}>
<Window.Content>
<Flex direction="column" height="100%">
<Flex.Item mb={1}>
<Section title="Servers">
<Box>
{webs.size > 0
? "Please select a techweb to connect to..."
: "Found no available techwebs!"}
</Box>
{webs.size > 0 && Array.from(webs, ([techweb, servers]) =>
<TechwebServer key={techweb} servers={servers} />)}
</Section>
</Flex.Item>
<Flex.Item mb={has_start_callback ? 1 : 0} grow={1}>
{servers.some(e => e.selected) && (
<Section title="Experiments"
className="ExperimentConfigure__ExperimentsContainer">
<Flex.Item mb={1}>
{experiments.length && always_active && (
"This device is configured to attempt to perform all available"
+ " experiments, so no further configuration is necessary."
) || experiments.length && (
"Select one of the following experiments..."
) || (
"No experiments found on this web"
)}
</Flex.Item>
<Flex.Item>
{experiments.map((exp, i) => {
return (
<Experiment key={i} exp={exp} controllable />
);
})}
</Flex.Item>
</Section>
)}
</Flex.Item>
{!!has_start_callback && (
<Flex.Item>
<Button
fluid
className="ExperimentConfigure__PerformExperiment"
onClick={() => act("start_experiment_callback")}
disabled={!experiments.some(e => e.selected)}
icon="flask">
Perform Experiment
</Button>
</Flex.Item>
)}
</Flex>
</Window.Content>
</Window>
);
};
export const Experiment = (props, context) => {
const { act, data } = useBackend(context);
const {
exp,
controllable,
} = props;
const {
name,
description,
tag,
selectable,
selected,
progress,
performance_hint,
ref,
} = exp;
return (
<Box m={1} key={ref}
className="ExperimentConfigure__ExperimentPanel">
<Button fluid
onClick={() => controllable && (selected
? act("clear_experiment")
: act("select_experiment", { "ref": ref }))}
backgroundColor={selected ? "good" : "#40628a"}
className="ExperimentConfigure__ExperimentName"
disabled={controllable && !selectable}>
<Flex align="center" justify="space-between">
<Flex.Item
color={!controllable || selectable
? "white"
: "rgba(0, 0, 0, 0.6)"}>
{name}
</Flex.Item>
<Flex.Item
color={!controllable || selectable
? "rgba(255, 255, 255, 0.5)"
: "rgba(0, 0, 0, 0.5)"}>
<Box className="ExperimentConfigure__TagContainer">
{tag}
<Icon
name="question-circle"
mx={0.5} />
<Box className="ExperimentConfigure__PerformanceHint">
<Tooltip
content={performance_hint}
position="bottom-left" />
</Box>
</Box>
</Flex.Item>
</Flex>
</Button>
<Box className={"ExperimentConfigure__ExperimentContent"}>
<Box mb={1}>
{description}
</Box>
{props.children}
<ExperimentStages>
{progress}
</ExperimentStages>
</Box>
</Box>
);
};
@@ -0,0 +1,81 @@
import { sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { classes } from 'common/react';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, LabeledList, Section, Stack } from '../components';
import { Window } from '../layouts';
import { capitalize } from 'common/string';
export const FishCatalog = (props, context) => {
const { act, data } = useBackend(context);
const {
fish_info,
sponsored_by,
} = data;
const fish_by_name = flow([
sortBy(fish => fish.name),
])(data.fish_info || []);
const [
currentFish,
setCurrentFish,
] = useLocalState(context, 'currentFish', null);
return (
<Window
width={500}
height={300}>
<Window.Content>
<Stack fill>
<Stack.Item width="120px">
<Section fill scrollable>
{fish_by_name.map(f => (
<Button
key={f.name}
fluid
color="transparent"
selected={f === currentFish}
onClick={() => { setCurrentFish(f); }}>
{f.name}
</Button>
))}
</Section>
</Stack.Item>
<Stack.Item grow basis={0}>
<Section
fill
scrollable
title={currentFish
? capitalize(currentFish.name)
: sponsored_by + " Fish Index"}>
{currentFish && (
<LabeledList>
<LabeledList.Item label="Description">
{currentFish.desc}
</LabeledList.Item>
<LabeledList.Item label="Water type">
{currentFish.fluid}
</LabeledList.Item>
<LabeledList.Item label="Temperature">
{currentFish.temp_min} to {currentFish.temp_max}
</LabeledList.Item>
<LabeledList.Item label="Feeding">
{currentFish.feed}
</LabeledList.Item>
<LabeledList.Item label="Acquisition">
{currentFish.source}
</LabeledList.Item>
<LabeledList.Item label="Illustration">
<Box
className={classes([
'fish32x32',
currentFish.icon,
])} />
</LabeledList.Item>
</LabeledList>
)}
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
+51
View File
@@ -0,0 +1,51 @@
import { useBackend } from "../backend";
import { Box, Button, Flex, Section } from "../components";
import { Window } from "../layouts";
export const Folder = (props, context) => {
const { act, data } = useBackend(context);
const { theme, bg_color, folder_name, contents, contents_ref } = data;
return (
<Window
title={folder_name || "Folder"}
theme={theme}
width={400}
height={500}
>
<Window.Content backgroundColor={bg_color || "#7f7f7f"} scrollable>
{contents.map((item, index) => (
<Flex
key={contents_ref[index]}
color="black"
backgroundColor="white"
style={{ padding: "2px 2px 0 2px" }}
mb={0.5}
>
<Flex.Item align="center" grow={1}>
<Box align="center">{item}</Box>
</Flex.Item>
<Flex.Item>
{
<Button
icon="search"
onClick={() => act("examine", { ref: contents_ref[index] })}
/>
}
<Button
icon="eject"
onClick={() => act("remove", { ref: contents_ref[index] })}
/>
</Flex.Item>
</Flex>
))}
{contents.length === 0 && (
<Section>
<Box color="lightgrey" align="center">
This folder is empty!
</Box>
</Section>
)}
</Window.Content>
</Window>
);
};
@@ -0,0 +1,118 @@
import { useBackend } from '../backend';
import { Box, Button, ColorBox, Flex, Icon, Input, LabeledList, Section, Stack, Table } from '../components';
import { Window } from '../layouts';
type ColorEntry = {
index: Number;
value: string;
}
type SpriteData = {
finished: SpriteEntry
steps: Array<SpriteEntry>
}
type SpriteEntry = {
layer: string
result: string
}
type GreyscaleMenuData = {
colors: Array<ColorEntry>;
sprites: SpriteData;
}
const ColorDisplay = (props, context) => {
const { act, data } = useBackend<GreyscaleMenuData>(context);
const colors = (data.colors || []);
return (
<Section title="Colors">
<LabeledList>
<LabeledList.Item
label="Full Color String">
<Input
value={colors.map(item => item.value).join('')}
onChange={(_, value) => act("recolor_from_string", { color_string: value })}
/>
</LabeledList.Item>
{colors.map(item => (
<LabeledList.Item
key={`colorgroup${item.index}${item.value}`}
label={`Color Group ${item.index}`}
color={item.value}
>
<ColorBox
color={item.value}
/>
{" "}
<Button
content={<Icon name="palette" />}
onClick={() => act("pick_color", { color_index: item.index })}
/>
<Input
value={item.value}
onChange={(_, value) => act("recolor", { color_index: item.index, new_color: value })}
/>
</LabeledList.Item>
))}
</LabeledList>
</Section>
);
};
const PreviewDisplay = (props, context) => {
const { data } = useBackend<GreyscaleMenuData>(context);
return (
<Section title="Preview">
<Table>
<Table.Row header>
<Table.Cell textAlign="center">Step Layer</Table.Cell>
<Table.Cell textAlign="center">Step Result</Table.Cell>
</Table.Row>
{data.sprites.steps.map(item => (
<Table.Row key={`${item.result}|${item.layer}`}>
<Table.Cell width="50%"><SingleSprite source={item.result} /></Table.Cell>
<Table.Cell width="50%"><SingleSprite source={item.layer} /></Table.Cell>
</Table.Row>
))}
</Table>
</Section>
);
};
const SingleSprite = (props) => {
const {
source,
} = props;
return (
<Box
as="img"
src={source}
width="100%"
/>
);
};
export const GreyscaleModifyMenu = (props, context) => {
const { act, data } = useBackend<GreyscaleMenuData>(context);
return (
<Window
title="Greyscale Modification"
width={325}
height={800}>
<Window.Content scrollable>
<ColorDisplay />
<Button
content="Refresh Icon File"
onClick={() => act("refresh_file")}
/>
{" "}
<Button
content="Apply"
onClick={() => act("apply")}
/>
<PreviewDisplay />
</Window.Content>
</Window>
);
};
+336
View File
@@ -0,0 +1,336 @@
import { filter, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { toFixed } from 'common/math';
import { useBackend } from '../backend';
import { Button, LabeledList, NumberInput, ProgressBar, Section, Stack, Box } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { formatSiBaseTenUnit, formatSiUnit } from '../format';
import { Window } from '../layouts';
export const Hypertorus = (props, context) => {
const { act, data } = useBackend(context);
const filterTypes = data.filter_types || [];
const selectedFuels = data.selected_fuel || [];
const {
energy_level,
core_temperature,
internal_power,
power_output,
heat_limiter_modifier,
heat_output,
heat_output_bool,
heating_conductor,
magnetic_constrictor,
fuel_injection_rate,
moderator_injection_rate,
current_damper,
power_level,
iron_content,
integrity,
start_power,
start_cooling,
start_fuel,
internal_fusion_temperature,
moderator_internal_temperature,
internal_output_temperature,
internal_coolant_temperature,
waste_remove,
selected,
product_gases,
} = data;
const fusion_gases = flow([
filter(gas => gas.amount >= 0.01),
sortBy(gas => -gas.amount),
])(data.fusion_gases || []);
const moderator_gases = flow([
filter(gas => gas.amount >= 0.01),
sortBy(gas => -gas.amount),
])(data.moderator_gases || []);
const fusionMax = Math.max(1, ...fusion_gases.map(gas => gas.amount));
const moderatorMax = Math.max(1, ...moderator_gases.map(gas => gas.amount));
return (
<Window
title="Fusion Reactor"
width={500}
height={600}>
<Window.Content scrollable>
<Section title="Switches">
<Stack>
<Stack.Item color="label">
{'Start power: '}
<Button
disabled={data.power_level > 0}
icon={data.start_power ? 'power-off' : 'times'}
content={data.start_power ? 'On' : 'Off'}
selected={data.start_power}
onClick={() => act('start_power')} />
</Stack.Item>
<Stack.Item color="label">
{'Start cooling: '}
<Button
disabled={start_fuel === 1
|| start_power === 0
|| (start_cooling && data.power_level > 0)}
icon={data.start_cooling ? 'power-off' : 'times'}
content={data.start_cooling ? 'On' : 'Off'}
selected={data.start_cooling}
onClick={() => act('start_cooling')} />
</Stack.Item>
<Stack.Item color="label">
{'Start fuel injection: '}
<Button
disabled={start_power === 0
|| start_cooling === 0}
icon={data.start_fuel ? 'power-off' : 'times'}
content={data.start_fuel ? 'On' : 'Off'}
selected={data.start_fuel}
onClick={() => act('start_fuel')} />
</Stack.Item>
</Stack>
</Section>
<Section>
<LabeledList>
<LabeledList.Item label="Fuel">
{selectedFuels.map(recipe => (
<Button
disabled={data.power_level > 0}
key={recipe.id}
selected={recipe.id === selected}
content={recipe.name}
onClick={() => act('fuel', {
mode: recipe.id,
})} />
))}
</LabeledList.Item>
<LabeledList.Item label="Gases">
<Box m={1} style={{
'white-space': 'pre-wrap',
}}>
{product_gases}
</Box>
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Internal Fusion Gases">
<LabeledList>
{fusion_gases.map(gas => (
<LabeledList.Item
key={gas.name}
label={getGasLabel(gas.name)}>
<ProgressBar
color={getGasColor(gas.name)}
value={gas.amount}
minValue={0}
maxValue={fusionMax}>
{toFixed(gas.amount, 2) + ' moles'}
</ProgressBar>
</LabeledList.Item>
))}
</LabeledList>
</Section>
<Section title="Moderator Gases">
<LabeledList>
{moderator_gases.map(gas => (
<LabeledList.Item
key={gas.name}
label={getGasLabel(gas.name)}>
<ProgressBar
color={getGasColor(gas.name)}
value={gas.amount}
minValue={0}
maxValue={moderatorMax}>
{toFixed(gas.amount, 2) + ' moles'}
</ProgressBar>
</LabeledList.Item>
))}
</LabeledList>
</Section>
<Section title="Reactor Parameters">
<LabeledList>
<LabeledList.Item label="Power Level">
<ProgressBar
value={power_level}
ranges={{
good: [0, 2],
average: [2, 4],
bad: [4, 6],
}} />
</LabeledList.Item>
<LabeledList.Item label="Integrity">
<ProgressBar
value={integrity / 100}
ranges={{
good: [0.90, Infinity],
average: [0.5, 0.90],
bad: [-Infinity, 0.5],
}} />
</LabeledList.Item>
<LabeledList.Item label="Iron Content">
<ProgressBar
value={iron_content}
ranges={{
good: [-Infinity, 3],
average: [3, 6],
bad: [6, Infinity],
}} />
</LabeledList.Item>
<LabeledList.Item label="Energy Levels">
<ProgressBar
color={'yellow'}
value={energy_level}
minValue={0}
maxValue={1e35}>
{formatSiUnit(energy_level, 1, 'J')}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Heat Limiter Modifier">
<ProgressBar
color={'blue'}
value={heat_limiter_modifier}
minValue={-1e40}
maxValue={1e30}>
{formatSiBaseTenUnit(heat_limiter_modifier * 1000, 1, 'K')}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Heat Output">
<ProgressBar
color={'grey'}
value={heat_output}
minValue={-1e40}
maxValue={1e30}>
{heat_output_bool + formatSiBaseTenUnit(heat_output * 1000, 1, 'K')}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Temperatures">
<LabeledList>
<LabeledList.Item label="Fusion gas temperature">
<ProgressBar
color={'yellow'}
value={internal_fusion_temperature}
minValue={0}
maxValue={1e30}>
{formatSiBaseTenUnit(internal_fusion_temperature * 1000, 1, 'K')}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Moderator gas temperature">
<ProgressBar
color={'red'}
value={moderator_internal_temperature}
minValue={0}
maxValue={1e30}>
{formatSiBaseTenUnit(moderator_internal_temperature * 1000, 1, 'K')}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Output gas temperature">
<ProgressBar
color={'pink'}
value={internal_output_temperature}
minValue={0}
maxValue={1e30}>
{formatSiBaseTenUnit(internal_output_temperature * 1000, 1, 'K')}
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Coolant output temperature">
<ProgressBar
color={'green'}
value={internal_coolant_temperature}
minValue={0}
maxValue={1e30}>
{formatSiBaseTenUnit(internal_coolant_temperature * 1000, 1, 'K')}
</ProgressBar>
</LabeledList.Item>
</LabeledList>
</Section>
<Section title="Tweakable Inputs">
<LabeledList>
<LabeledList.Item label="Heating Conductor">
<NumberInput
animated
value={parseFloat(data.heating_conductor)}
width="63px"
unit="J/cm"
minValue={50}
maxValue={500}
onDrag={(e, value) => act('heating_conductor', {
heating_conductor: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="Magnetic Constrictor">
<NumberInput
animated
value={parseFloat(data.magnetic_constrictor)}
width="63px"
unit="m^3/B"
minValue={50}
maxValue={1000}
onDrag={(e, value) => act('magnetic_constrictor', {
magnetic_constrictor: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="Fuel Injection Rate">
<NumberInput
animated
value={parseFloat(data.fuel_injection_rate)}
width="63px"
unit="g/s"
minValue={5}
maxValue={1500}
onDrag={(e, value) => act('fuel_injection_rate', {
fuel_injection_rate: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="Moderator Injection Rate">
<NumberInput
animated
value={parseFloat(data.moderator_injection_rate)}
width="63px"
unit="g/s"
minValue={5}
maxValue={1500}
onDrag={(e, value) => act('moderator_injection_rate', {
moderator_injection_rate: value,
})} />
</LabeledList.Item>
<LabeledList.Item label="Current Damper">
<NumberInput
animated
value={parseFloat(data.current_damper)}
width="63px"
unit="W"
minValue={0}
maxValue={1000}
onDrag={(e, value) => act('current_damper', {
current_damper: value,
})} />
</LabeledList.Item>
</LabeledList>
</Section>
<Section>
<LabeledList>
<LabeledList.Item label="Waste remove">
<Button
disabled={data.power_level > 5}
icon={data.waste_remove ? 'power-off' : 'times'}
content={data.waste_remove ? 'On' : 'Off'}
selected={data.waste_remove}
onClick={() => act('waste_remove')} />
</LabeledList.Item>
<LabeledList.Item label="Filter from moderator mix">
{filterTypes.map(filter => (
<Button
key={filter.id}
selected={filter.selected}
content={getGasLabel(filter.id, filter.name)}
onClick={() => act('filter', {
mode: filter.id,
})} />
))}
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,91 @@
import { Button, TextArea, Section, BlockQuote, NoticeBox } from '../components';
import { Window } from '../layouts';
import { useBackend } from '../backend';
export const Interview = (props, context) => {
const { act, data } = useBackend(context);
const {
welcome_message,
questions,
read_only,
queue_pos,
is_admin,
status,
connected,
} = data;
const rendered_status = status => {
switch (status) {
case "interview_approved":
return (<NoticeBox success>This interview was approved.</NoticeBox>);
case "interview_denied":
return (<NoticeBox danger>This interview was denied.</NoticeBox>);
default:
return (<NoticeBox info>
Your answers have been submitted. You are position {queue_pos} in
queue.</NoticeBox>);
}
};
return (
<Window
width={500}
height={600}
noClose={!is_admin}>
<Window.Content scrollable>
{(!read_only && (
<Section title="Welcome!">
<p>
{welcome_message}
</p>
</Section>)) || rendered_status(status)}
<Section
title="Questionnaire"
buttons={(
<span>
<Button
content={read_only ? "Submitted" : "Submit"}
onClick={() => act('submit')}
disabled={read_only} />
{!!is_admin && status === "interview_pending" && (
<span>
<Button content="Admin PM"
enabled={connected} onClick={() => act('adminpm')} />
<Button content="Approve"
color="good" onClick={() => act('approve')} />
<Button content="Deny"
color="bad" onClick={() => act('deny')} />
</span>
)}
</span>
)}>
{!read_only && (
<p>
Please answer the following questions,
and press submit when you are satisfied with your answers.
<br /><br />
<b>You will not be able to edit your answers after submitting.</b>
</p>)}
{questions.map(({ qidx, question, response }) => (
<Section key={qidx} title={`Question ${qidx}`}>
<p>{question}</p>
{(read_only && (
<BlockQuote>{response || "No response."}</BlockQuote>)) || (
<TextArea
value={response}
fluid
height={10}
maxLength={500}
placeholder="Write your response here, max of 500 characters."
onChange={(e, input) => input !== response
&& act('update_answer', {
qidx: qidx,
answer: input,
})} />)}
</Section>)
)}
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,49 @@
import { Button, Section } from '../components';
import { Window } from '../layouts';
import { useBackend } from '../backend';
export const InterviewManager = (props, context) => {
const { act, data } = useBackend(context);
const {
open_interviews,
closed_interviews,
} = data;
const colorMap = status => {
switch (status) {
case "interview_approved":
return "good";
case "interview_denied":
return "bad";
case "interview_pending":
return "average";
}
};
return (
<Window
width={500}
height={600}>
<Window.Content scrollable>
<Section title="Active Interviews">
{open_interviews.map(({ id, ckey, status, queued, disconnected }) => (
<Button
key={id}
content={ckey + (disconnected ? " (DC)" : "")}
color={queued ? "default" : colorMap(status)}
onClick={() => act('open', { "id": id })} />
))}
</Section>
<Section title="Closed Interviews">
{closed_interviews.map(({ id, ckey, status, disconnected }) => (
<Button
key={id}
content={ckey + (disconnected ? " (DC)" : "")}
color={colorMap(status)}
onClick={() => act('open', { "id": id })} />
))}
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,94 @@
import { useBackend, useSharedState } from '../backend';
import { Box, Button, Dimmer, Icon, LabeledList, Section, Tabs } from '../components';
import { Window } from '../layouts';
export const Limbgrower = (props, context) => {
const { act, data } = useBackend(context);
const {
reagents = [],
total_reagents,
max_reagents,
categories = [],
busy,
} = data;
const [tab, setTab] = useSharedState(
context, 'category', categories[0]?.name);
const designList = categories
.find(category => category.name === tab)
?.designs;
return (
<Window
title="Limb Grower"
width={400}
height={550}>
{!!busy && (
<Dimmer fontSize="32px">
<Icon name="cog" spin={1} />
{' Building...'}
</Dimmer>
)}
<Window.Content scrollable>
<Section title="Reagents">
<Box mb={1}>
{total_reagents} / {max_reagents} reagent capacity used.
</Box>
<LabeledList>
{reagents.map(reagent => (
<LabeledList.Item
key={reagent.reagent_name}
label={reagent.reagent_name}
buttons={(
<Button.Confirm
textAlign="center"
width="120px"
content="Remove Reagent"
color="bad"
onClick={() => act('empty_reagent', {
reagent_type: reagent.reagent_type,
})} />
)}>
{reagent.reagent_amount}u
</LabeledList.Item>
))}
</LabeledList>
</Section>
<Section title="Designs">
<Tabs>
{categories.map(category => (
<Tabs.Tab
fluid
key={category.name}
selected={tab === category.name}
onClick={() => setTab(category.name)}>
{category.name}
</Tabs.Tab>
))}
</Tabs>
<LabeledList>
{designList.map(design => (
<LabeledList.Item
key={design.name}
label={design.name}
buttons={(
<Button
content="Make"
color="good"
onClick={() => act('make_limb', {
design_id: design.id,
active_tab: design.parent_category,
})} />
)}>
{design.needed_reagents.map(reagent => (
<Box key={reagent.name}>
{reagent.name}: {reagent.amount}u
</Box>
))}
</LabeledList.Item>
))}
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
+206
View File
@@ -0,0 +1,206 @@
/**
* @file
* @copyright 2020 watermelon914 (https://github.com/watermelon914)
* @license MIT
*/
import { clamp01 } from 'common/math';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Section, Input, Stack } from '../components';
import { Window } from '../layouts';
const ARROW_KEY_UP = 38;
const ARROW_KEY_DOWN = 40;
let lastScrollTime = 0;
export const ListInput = (props, context) => {
const { act, data } = useBackend(context);
const {
title,
message,
buttons,
timeout,
} = data;
// Search
const [showSearchBar, setShowSearchBar] = useLocalState(
context, 'search_bar', false);
const [displayedArray, setDisplayedArray] = useLocalState(
context, 'displayed_array', buttons);
// KeyPress
const [searchArray, setSearchArray] = useLocalState(
context, 'search_array', []);
const [searchIndex, setSearchIndex] = useLocalState(
context, 'search_index', 0);
const [lastCharCode, setLastCharCode] = useLocalState(
context, 'last_char_code', null);
// Selected Button
const [selectedButton, setSelectedButton] = useLocalState(
context, 'selected_button', buttons[0]);
const handleKeyDown = e => {
e.preventDefault();
if (lastScrollTime > performance.now()) {
return;
}
lastScrollTime = performance.now() + 125;
if (e.keyCode === ARROW_KEY_UP || e.keyCode === ARROW_KEY_DOWN) {
let direction = 1;
if (e.keyCode === ARROW_KEY_UP) direction = -1;
let index = 0;
for (index; index < buttons.length; index++) {
if (buttons[index] === selectedButton) break;
}
index += direction;
if (index < 0) index = buttons.length - 1;
else if (index >= buttons.length) index = 0;
setSelectedButton(buttons[index]);
setLastCharCode(null);
document.getElementById(buttons[index]).focus();
return;
}
const charCode = String.fromCharCode(e.keyCode).toLowerCase();
if (!charCode) return;
let foundValue;
if (charCode === lastCharCode && searchArray.length > 0) {
const nextIndex = searchIndex + 1;
if (nextIndex < searchArray.length) {
foundValue = searchArray[nextIndex];
setSearchIndex(nextIndex);
}
else {
foundValue = searchArray[0];
setSearchIndex(0);
}
}
else {
const resultArray = displayedArray.filter(value =>
value.substring(0, 1).toLowerCase() === charCode
);
if (resultArray.length > 0) {
setSearchArray(resultArray);
setSearchIndex(0);
foundValue = resultArray[0];
}
}
if (foundValue) {
setLastCharCode(charCode);
setSelectedButton(foundValue);
document.getElementById(foundValue).focus();
}
};
return (
<Window
title={title}
width={325}
height={325}>
{timeout !== undefined && <Loader value={timeout} />}
<Window.Content>
<Stack fill vertical>
<Stack.Item grow>
<Section
fill
scrollable
className="ListInput__Section"
title={message}
tabIndex={0}
onKeyDown={handleKeyDown}
buttons={(
<Button
compact
icon="search"
color="transparent"
selected={showSearchBar}
tooltip="Search Bar"
tooltipPosition="left"
onClick={() => {
setShowSearchBar(!showSearchBar);
setDisplayedArray(buttons);
}}
/>
)}>
{displayedArray.map(button => (
<Button
key={button}
fluid
color="transparent"
id={button}
selected={selectedButton === button}
onClick={() => {
if (selectedButton === button) {
act("choose", { choice: button });
}
else {
setSelectedButton(button);
}
setLastCharCode(null);
}}>
{button}
</Button>
))}
</Section>
</Stack.Item>
{showSearchBar && (
<Stack.Item>
<Input
fluid
onInput={(e, value) => setDisplayedArray(
buttons.filter(val => (
val.toLowerCase().search(value.toLowerCase()) !== -1
))
)}
/>
</Stack.Item>
)}
<Stack.Item>
<Stack textAlign="center">
<Stack.Item grow basis={0}>
<Button
fluid
color="good"
lineHeight={2}
content="Confirm"
disabled={selectedButton === null}
onClick={() => act("choose", { choice: selectedButton })}
/>
</Stack.Item>
<Stack.Item grow basis={0}>
<Button
fluid
color="bad"
lineHeight={2}
content="Cancel"
onClick={() => act("cancel")}
/>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
export const Loader = props => {
const { value } = props;
return (
<div className="ListInput__Loader">
<Box
className="ListInput__LoaderProgress"
style={{
width: clamp01(value) * 100 + '%',
}} />
</div>
);
};
+290
View File
@@ -0,0 +1,290 @@
import { round } from 'common/math';
import { useBackend } from '../backend';
import { Box, Button, Dimmer, Icon, Section, Slider, Table } from '../components';
import { Window } from '../layouts';
export const MassSpec = (props, context) => {
const { act, data } = useBackend(context);
const {
processing,
lowerRange,
upperRange,
graphUpperRange,
graphLowerRange,
eta,
beaker1CurrentVolume,
beaker2CurrentVolume,
beaker1MaxVolume,
beaker2MaxVolume,
peakHeight,
beaker1,
beaker2,
beaker1Contents = [],
beaker2Contents = [],
} = data;
const centerValue = (lowerRange + upperRange) / 2;
return (
<Window
width={490}
height={650}>
<Window.Content scrollable>
{!!processing && (
<Dimmer fontSize="32px">
<Icon name="cog" spin={1} />
{' Purifying... '+round(eta)+"s"}
</Dimmer>
)}
<Section
title="Mass Spectroscopy"
buttons={
<Button
icon="power-off"
content="Start"
disabled={!!processing || !beaker1Contents.length || !beaker2}
tooltip={!beaker1Contents.length ? "Missing input reagents!" : !beaker2 ? "Missing an output beaker!" : "Begin purifying"}
tooltipPosition="left"
onClick={() => act('activate')} />
}>
{beaker1Contents.length && (
<MassSpectroscopy
lowerRange={lowerRange}
centerValue={centerValue}
upperRange={upperRange}
graphLowerRange={graphLowerRange}
graphUpperRange={graphUpperRange}
maxAbsorbance={peakHeight}
reagentPeaks={beaker1Contents} />
) || (
<Box>
Please insert an input beaker with reagents!
</Box>
)}
</Section>
<Section
title="Input beaker"
buttons={!!beaker1Contents && (
<>
{!!beaker1MaxVolume && (
<Box inline color="label" mr={2}>
{beaker1CurrentVolume} / {beaker1MaxVolume} units
</Box>
)}
<Button
icon="eject"
content="Eject"
disabled={!beaker1}
onClick={() => act('eject1')} />
</>
)}>
<BeakerMassProfile
loaded={!!beaker1}
beaker={beaker1Contents} />
{!!beaker1Contents.length && (
<Box>
{"Eta of selection: " + round(eta) + " seconds"}
</Box>
)}
</Section>
<Section
title="Output beaker"
buttons={!!beaker2Contents && (
<>
{!!beaker2MaxVolume && (
<Box inline color="label" mr={2}>
{beaker2CurrentVolume} / {beaker2MaxVolume} units
</Box>
)}
<Button
icon="eject"
content="Eject"
disabled={!beaker2}
onClick={() => act('eject2')} />
</>
)}>
<BeakerMassProfile
loaded={!!beaker2}
beaker={beaker2Contents}
details />
</Section>
</Window.Content>
</Window>
);
};
const BeakerMassProfile = props => {
const {
loaded,
details,
beaker = [],
} = props;
return (
<Box>
{!loaded && (
<Box color="label">
No beaker loaded.
</Box>
) || beaker.length === 0 && (
<Box color="label">
Beaker is empty.
</Box>
) || (
<Table className="candystripe">
<Table.Row>
<Table.Cell bold collapsing color="label">
Reagent
</Table.Cell>
<Table.Cell bold collapsing color="label">
Volume
</Table.Cell>
<Table.Cell bold collapsing color="label">
Mass
</Table.Cell>
<Table.Cell bold collapsing color="label">
Type
</Table.Cell>
{!!details && (
<Table.Cell bold collapsing color="label">
Results
</Table.Cell>
)}
</Table.Row>
{beaker.map(reagent => (
<Table.Row key={reagent.name}>
<Table.Cell collapsing color={reagent.selected ? "green" : "default"}>
{reagent.name}
</Table.Cell>
<Table.Cell collapsing color={reagent.selected ? "green" : "default"}>
{reagent.volume}
</Table.Cell>
<Table.Cell collapsing color={reagent.selected ? "green" : "default"}>
{reagent.mass}
</Table.Cell>
<Table.Cell collapsing color={reagent.color}>
{reagent.type}
</Table.Cell>
{!!details && (
<Table.Cell>
{reagent.log}
</Table.Cell>
)}
</Table.Row>
))}
</Table>
)}
</Box>
);
};
const MassSpectroscopy = (props, context) => {
const { act, data } = useBackend(context);
const {
lowerRange,
centerValue,
upperRange,
graphUpperRange,
graphLowerRange,
maxAbsorbance,
reagentPeaks = [],
} = props;
const deltaRange = graphUpperRange - graphLowerRange;
const graphIncrement = deltaRange * 0.2;
return (
<>
<Box position="absolute" x="200" transform="translate(30,30)">
<svg background-size="200px" width="200" height="400">
<text x="0" y="250" text-anchor="middle" fill="white" font-size="16" transform="translate(0,0) scale(0.8 0.8)">
{/* x axis*/}
<tspan x="250" y="318" font-weight="bold" font-size="1.4em">Mass (g)</tspan>
<tspan x="0" y="283">{graphLowerRange}</tspan>
<tspan x="100" y="283">{round(graphLowerRange + (graphIncrement), 1)}</tspan>
<tspan x="200" y="283">{round(graphLowerRange + (graphIncrement * 2), 1)}</tspan>
<tspan x="300" y="283">{round(graphLowerRange + (graphIncrement * 3), 1)}</tspan>
<tspan x="400" y="283">{round(graphLowerRange + (graphIncrement * 4), 1)}</tspan>
<tspan x="500" y="283">{graphUpperRange}</tspan>
{/* y axis*/}
<tspan x="520" y="0" dy="6">{round(maxAbsorbance, 1)}</tspan>
<tspan x="520" y="50" dy="6">{round(maxAbsorbance * 0.8, 1)}</tspan>
<tspan x="520" y="100" dy="6">{round(maxAbsorbance * 0.6, 1)}</tspan>
<tspan x="520" y="150" dy="6">{round(maxAbsorbance * 0.4, 1)}</tspan>
<tspan x="520" y="200" dy="6">{round(maxAbsorbance * 0.2, 1)}</tspan>
<tspan x="520" y="250" dy="6">0</tspan>
</text>
<text text-anchor="middle" transform="translate(430,100) rotate(90) scale(0.8 0.8)" fill="white" font-size="16">
<tspan font-weight="bold" font-size="1.4em">Absorbance (AU)</tspan>
</text>
<g transform="translate(0, 0) scale(0.8 0.8)">
{reagentPeaks.map(peak => (
// Triangle peak
<polygon key={peak.name} points={`${((peak.mass - 10) / graphUpperRange) * 500},265 ${((peak.mass) / graphUpperRange) * 500},${250 - ((peak.volume / maxAbsorbance) * 250)} ${((peak.mass + 10) / graphUpperRange) * 500},265 `} opacity="0.6" style={`fill:${peak.color}`} />
))}
<polygon points={`${(lowerRange/deltaRange)*500},265 ${(lowerRange/deltaRange)*500},0 ${(upperRange/deltaRange)*500},0 ${(upperRange/deltaRange)*500},265`} opacity="0.2" style={`fill:blue`} />
<line x1={0} y1={265} x2={502} y2={264} stroke={"white"} stroke-width={3} />
<line x1={501} y1={264} x2={501} y2={0} stroke={"white"} stroke-width={3} />
</g>
</svg>
</Box>
<Box>
<Slider
name={"Left slider"}
position="relative"
step={graphUpperRange/400}
height={17.2}
format={value => round(value)}
width={(centerValue/graphUpperRange)*400+"px"}
value={lowerRange}
minValue={graphLowerRange}
maxValue={centerValue}
color={"invisible"}
onDrag={(e, value) => act('leftSlider', {
value: value,
})} >
{" "}
</Slider>
<Slider
name={"Right slider"}
position="absolute"
height={17.2}
format={value => round(value)}
step={graphUpperRange/400}
width={400-((centerValue/graphUpperRange)*400)+"px"}
value={upperRange}
minValue={centerValue}
maxValue={graphUpperRange}
color={"invisible"}
onDrag={(e, value) => act('rightSlider', {
value: value,
})} >
{" "}
</Slider>
<Box>
<Slider
name={"Center slider"}
position="relative"
step={graphUpperRange/400}
mt={0.3}
mb={5}
value={centerValue}
height={1.9}
format={value => round(value)}
width={400+"px"}
minValue={graphLowerRange + 1}
maxValue={graphUpperRange - 1}
color={"invisible"}
onDrag={(e, value) => act('centerSlider', {
value: value,
})} >
{" "}
</Slider>
</Box>
</Box>
</>
);
};
@@ -0,0 +1,13 @@
import { AppTechweb } from './Techweb.js';
import { useBackend, useLocalState } from '../backend';
import { createLogger } from '../logging';
const logger = createLogger('backend');
export const NtosTechweb = (props, context) => {
const { config, data, act } = useBackend(context);
logger.log(config.AppTechweb);
return (
<AppTechweb />
);
};
+636
View File
@@ -0,0 +1,636 @@
import { multiline } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Divider, Icon, NumberInput, Section, Stack } from '../components';
import { Window } from '../layouts';
const buttonWidth = 2;
const goodstyle = {
color: 'lightgreen',
fontWeight: 'bold',
};
const badstyle = {
color: 'red',
fontWeight: 'bold',
};
const partstyle = {
color: 'yellow',
fontWeight: 'bold',
};
const fuelstyle = {
color: 'olive',
fontWeight: 'bold',
};
const variousButtonIcons = {
"Restore Hull": "wrench",
"Fix Engine": "rocket",
"Repair Electronics": "server",
"Wait": "clock",
"Continue": "arrow-right",
"Explore Ship": "door-open",
"Leave the Derelict": "arrow-right",
"Welcome aboard.": "user-plus",
"Where did you go?!": "user-minus",
"A good find.": "box-open",
"Continue travels.": "arrow-right",
"Keep Speed": "tachometer-alt",
"Slow Down": "arrow-left",
"Speed Past": "tachometer-alt",
"Go Around": "redo",
"Oh...": "circle",
"Dock": "dollar-sign",
};
const STATUS2COMPONENT = [
{ component: () => ORION_STATUS_START },
{ component: () => ORION_STATUS_INSTRUCTIONS },
{ component: () => ORION_STATUS_NORMAL },
{ component: () => ORION_STATUS_GAMEOVER },
{ component: () => ORION_STATUS_MARKET },
];
const locationInfo = [
{
title: "Pluto",
blurb: "Pluto, long since occupied with long-range sensors and scanners, stands ready to, and indeed continues to probe the far reaches of the galaxy.",
},
{
title: "Asteroid Belt",
blurb: "At the edge of the Sol system lies a treacherous asteroid belt. Many have been crushed by stray asteroids and misguided judgement.",
},
{
title: "Proxima Centauri",
blurb: "The nearest star system to Sol, in ages past it stood as a reminder of the boundaries of sub-light travel, now a low-population sanctuary for adventurers and traders.",
},
{
title: "Dead Space",
blurb: "This region of space is particularly devoid of matter. Such low-density pockets are known to exist, but the vastness of it is astounding.",
},
{
title: "Rigel Prime",
blurb: "Rigel Prime, the center of the Rigel system, burns hot, basking its planetary bodies in warmth and radiation.",
},
{
title: "Tau Ceti Beta",
blurb: "Tau Ceti Beta has recently become a waypoint for colonists headed towards Orion. There are many ships and makeshift stations in the vicinity.",
},
{
title: "Space Bugs",
blurb: "You see some space bugs out your window. They contort in various reality bending ways, and it makes you sick. You know it's Galactic Policy to report all sightings of space bugs.",
},
{
title: "Space Outpost Beta-9",
blurb: "You have come into range of the first man-made structure in this region of space. It has been constructed not by travellers from Sol, but by colonists from Orion. It stands as a monument to the colonists' success.",
},
{
title: "Orion Prime",
blurb: "You have made it to Orion! Congratulations! Your crew is one of the few to start a new foothold for mankind!",
},
];
const AdventureStatus = (props, context) => {
const { data, act } = useBackend(context);
const {
lings_suspected,
eventname,
settlers,
settlermoods,
hull,
electronics,
engine,
food,
fuel,
} = data;
return (
<Section
title="Adventure Status"
fill
buttons={(
!!lings_suspected && (
<Button
fluid
color="black"
textAlign="center"
icon="skull"
content="RANDOM KILL"
disabled={eventname}
onClick={() => act('random_kill')} />
)
)} >
<Stack mb={-1} fill>
<Stack.Item grow mb={-0.5}>
{settlers?.map(settler => (
<Stack key={settler}>
<Stack.Item grow mt={0.9}>
{settler}
</Stack.Item>
<Stack.Item mt={0.9}>
<Button
fluid
color="red"
textAlign="center"
icon="skull"
content="KILL"
disabled={lings_suspected || eventname}
onClick={() => act('target_kill', {
who: settler,
})} />
</Stack.Item>
<Stack.Item mr={0}>
<Box className={'moods32x32 mood' + (settlermoods[settler] + 1)} />
</Stack.Item>
</Stack>
))}
</Stack.Item>
<Divider vertical />
<Stack.Item>
<Stack vertical fill>
<Stack.Item>
<Button
fluid
icon="hamburger"
content={"Food Left: " + food}
color="green" />
</Stack.Item>
<Stack.Item>
<Button
fluid
icon="gas-pump"
content={"Fuel Left: " + fuel}
color="olive" />
</Stack.Item>
<Stack.Item>
<Button
fluid
icon="wrench"
content={"Hull Parts: "+hull}
color="average" />
</Stack.Item>
<Stack.Item>
<Button
fluid
icon="server"
content={"Electronics: "+electronics}
color="blue" />
</Stack.Item>
<Stack.Item mb={1}>
<Button
fluid
icon="rocket"
content={"Engine Parts: "+engine}
color="violet" />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const ORION_STATUS_START = (props, context) => {
const { data, act } = useBackend(context);
const {
gamename,
} = data;
return (
<Section fill>
<Stack vertical textAlign="center" fill>
<Stack.Item grow={1} />
<Stack.Item fontSize="32px">
{gamename}
</Stack.Item>
<Stack.Item grow fontSize="15px" color="label">
{"\"Experience the journey of your ancestors!\""}
</Stack.Item>
<Stack.Item fontSize="15px">
<Button
lineHeight={2}
fluid
icon="play"
content="Begin Game"
onClick={() => act('start_game')} />
</Stack.Item>
<Stack.Item fontSize="15px">
<Button
lineHeight={2}
fluid
icon="info"
content="Instructions"
onClick={() => act('instructions')} />
</Stack.Item>
<Stack.Item grow={3} />
</Stack>
</Section>
);
};
const ORION_STATUS_INSTRUCTIONS = (props, context) => {
const { act } = useBackend(context);
const fake_settlers = ["John", "William", "Alice", "Tom"];
return (
<Stack vertical fill>
<Stack.Item grow>
<Section
color="label"
title="Objective"
fill
buttons={(
<Button
content="Back to Main Menu"
onClick={() => act('back_to_menu')} />
)}>
<Box fontSize="11px">
In the 2200&apos;s, the Orion trail was established as a dangerous
yet opportunistic trail through space for those willing to risk it.
Many pioneers seeking new lives on the galactic frontier would find
exactly what they were seeking... or lose their lives on the way.
</Box>
</Section>
</Stack.Item>
<Stack.Item>
<Section title="Status Example" fill>
<Stack mb={-1} fill>
<Stack.Item basis={70} grow mb={-0.5}>
{fake_settlers?.map(settler => (
<Stack key={settler}>
<Stack.Item grow mt={0.9}>
{settler}
</Stack.Item>
<Stack.Item mt={0.9}>
<Button
fluid
color="red"
textAlign="center"
icon="skull"
content="KILL" />
</Stack.Item>
<Stack.Item mr={0}>
<Box className={'moods32x32 mood5'} />
</Stack.Item>
</Stack>
))}
</Stack.Item>
<Divider vertical />
<Stack.Item grow>
This is the status panel for your pioneers. Each one requires
1 food every time you continue
towards <span style={goodstyle}>Orion</span>.
You can find more crew on your journey, and lose them as
fast as you found &apos;em.
<br /><br />
If you run out of food or crew,
it&apos;s <span style={badstyle}>GAME OVER</span> for you!
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section fill title="Resources">
<Stack fill>
<Stack.Item grow mt={-1}>
If you want to make it to <span style={goodstyle}>Orion</span>,
you&apos;ll need to manage your resources:
<br />
<span style={goodstyle}>Food</span>: Your crewmembers consume
it. More crew means this goes down faster!
<br />
<span style={fuelstyle}>Fuel</span>: You use 5u of fuel with
every movement. Don&apos;t let it run out.
<br />
<span style={partstyle}>Parts</span>: Used to repair breakdowns.
Nobody likes wasting time on repairs!
</Stack.Item>
<Divider vertical />
<Stack.Item>
<Stack vertical fill>
<Stack.Item grow>
<Button
fluid
icon="hamburger"
content={"Food Left: 80"}
color="green" />
</Stack.Item>
<Stack.Item grow>
<Button
fluid
icon="gas-pump"
content={"Fuel Left: 60"}
color="olive" />
</Stack.Item>
<Stack.Item grow>
<Button
fluid
icon="wrench"
content={"Hull Parts: 1"}
color="average" />
</Stack.Item>
<Stack.Item grow>
<Button
fluid
icon="server"
content={"Electronics: 1"}
color="blue" />
</Stack.Item>
<Stack.Item mb={-0.3} grow>
<Button
fluid
icon="rocket"
content={"Engine Parts: 1"}
color="violet" />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
);
};
const ORION_STATUS_NORMAL = (props, context) => {
const { data, act } = useBackend(context);
const {
settlers,
settlermoods,
hull,
electronics,
engine,
food,
fuel,
turns,
eventname,
eventtext,
buttons,
} = data;
return (
<Stack vertical fill>
<Stack.Item grow>
<Section title={!!eventname && "Event" || "Location"} fill>
<Stack fill textAlign="center" vertical>
<Stack.Item grow >
<Box bold fontSize="15px">
{!!eventname && eventname || locationInfo[turns-1].title}
</Box>
<br />
<Box fontSize="15px">
{!!eventtext && eventtext || locationInfo[turns-1].blurb}
</Box>
</Stack.Item>
<Stack.Item>
{!!buttons && (
buttons.map(button => (
<Stack.Item key={button}>
<Button
mb={1}
lineHeight={3}
width={16}
icon={variousButtonIcons[button]}
content={button}
onClick={() => act(button)} />
</Stack.Item>
))
) || (
<Button
mb={1}
lineHeight={3}
width={16}
icon="arrow-right"
content="Continue"
onClick={() => act('continue')} />
)}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<AdventureStatus />
</Stack.Item>
</Stack>
);
};
const ORION_STATUS_GAMEOVER = (props, context) => {
const { data, act } = useBackend(context);
const {
reason,
} = data;
return (
<Section fill>
<Stack vertical textAlign="center" fill>
<Stack.Item grow={1} />
<Stack.Item color="red" fontSize="32px">
{"Game Over"}
</Stack.Item>
<Stack.Item grow fontSize="15px" color="label">
{reason}
</Stack.Item>
<Stack.Item fontSize="15px">
<Button
lineHeight={2}
fluid
icon="arrow-left"
content="Main Menu"
onClick={() => act('back_to_menu')} />
</Stack.Item>
<Stack.Item grow={3} />
</Stack>
</Section>
);
};
const marketButtonSpacing = 0.8;
const ORION_STATUS_MARKET = (props, context) => {
const { data, act } = useBackend(context);
const {
turns,
spaceport_raided,
} = data;
return (
<Stack vertical fill>
<Stack.Item grow>
<Section
title="Market"
fill
buttons={(
<>
<Button
content="Raid"
icon="skull"
color="black"
disabled={spaceport_raided}
onClick={() => act('raid_spaceport')} />
<Button
content="Leave"
icon="arrow-right"
onClick={() => act('leave_spaceport')} />
</>
)}>
<Stack fill textAlign="center" vertical>
<Stack.Item grow >
<Box mb={-2} bold fontSize="15px">
{turns === 4 && "Tau Ceti Beta" || "Small Space Port"}
</Box>
<br />
<Box fontSize="14px">
{spaceport_raided && (
<Box color="red">
You are lucky to have escaped with your life. Attempting
to dock again would be certain death.
</Box>
) || (
"Hello, Pioneer! We have supplies for you to help \
you reach Orion. They aren't free, though!"
)}
</Box>
</Stack.Item>
{spaceport_raided && (
<>
<Stack.Item>
The Port is under high security. Any possibility of
purchasing goods has long since sailed.
</Stack.Item>
<Stack.Item grow />
</>
) || (
<>
<Stack.Item>
General Markets:
</Stack.Item>
<Stack.Item>
<Stack mb={-1} fill>
<Stack.Item grow basis={0}>
<Stack vertical>
<Stack.Item>
<Button
fluid
icon="hamburger"
content={"5 Food for 5 Fuel"}
color="green"
onClick={() => act('trade', {
what: 2,
})} />
</Stack.Item>
<Divider />
<Stack.Item mt={0}>
Port Hangar Bay:
</Stack.Item>
<Stack.Item mb={marketButtonSpacing}>
<Button
fluid
icon="wrench"
content={"5 Fuel for Hull Plates"}
color="average"
onClick={() => act('buyparts', {
part: 2,
})} />
</Stack.Item>
<Stack.Item mb={marketButtonSpacing}>
<Button
fluid
icon="server"
content={"5 Fuel for Electronics"}
color="blue"
onClick={() => act('buyparts', {
part: 3,
})} />
</Stack.Item>
<Stack.Item mb={marketButtonSpacing}>
<Button
fluid
icon="rocket"
content={"5 Fuel for Engine Parts"}
color="violet"
onClick={() => act('buyparts', {
part: 1,
})} />
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow basis={0}>
<Stack vertical>
<Stack.Item>
<Button
fluid
icon="gas-pump"
content={"5 Fuel for 5 Food"}
color="olive"
onClick={() => act('trade', {
what: 1,
})} />
</Stack.Item>
<Divider />
<Stack.Item mt={0}>
Port Bar:
</Stack.Item>
<Stack.Item mb={marketButtonSpacing}>
<Button
fluid
icon="user-plus"
content={"10 Food, 10 Fuel for Crew"}
color="white"
onClick={() => act('buycrew')} />
</Stack.Item>
<Stack.Item mb={marketButtonSpacing}>
<Button
fluid
icon="user-minus"
content={"Crew for 7 Food, 7 Fuel"}
color="black"
onClick={() => act('sellcrew')} />
</Stack.Item>
<Stack.Item mb={marketButtonSpacing}>
<Button
fluid
icon="meteor"
content={"Odd Crew (Same Price)"}
color="purple"
onClick={() => act('buycrew', {
odd: 1,
})} />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Stack.Item>
</>
)}
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<AdventureStatus />
</Stack.Item>
</Stack>
);
};
export const OrionGame = (props, context) => {
const { act, data } = useBackend(context);
const {
gamestatus,
gamename,
eventname,
} = data;
const GameStatusComponent = STATUS2COMPONENT[gamestatus].component();
const MarketRaid = STATUS2COMPONENT[2].component();
return (
<Window
title={gamename}
width={400}
height={500}>
<Window.Content>
{eventname === "Space Port Raid" && (
<MarketRaid />
) || (
<GameStatusComponent />
)}
</Window.Content>
</Window>
);
};
@@ -0,0 +1,146 @@
import { useBackend } from '../backend';
import { Box, Button, Icon, Section, Stack } from '../components';
import { Window } from '../layouts';
export const OutfitEditor = (props, context) => {
const { act, data } = useBackend(context);
const { outfit, saveable, dummy64 } = data;
return (
<Window
width={380}
height={600}>
<Window.Content>
<Box
as="img"
fillPositionedParent
width="100%"
height="100%"
opacity={0.5}
py={3}
src={`data:image/jpeg;base64,${dummy64}`}
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
<Section
fill
title={
<Stack>
<Stack.Item grow={1}
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}>
<Button
ml={0.5}
color="transparent"
icon="pencil-alt"
title="Rename this outfit"
onClick={() => act("rename", {})} />
{outfit.name}
</Stack.Item>
<Stack.Item align="end" shrink={0}>
<Button
color="transparent"
icon="info"
tooltip="Ctrl-click a button to select *any* item instead of what will probably fit in that slot."
tooltipPosition="bottom-left" />
<Button
icon="code"
tooltip="Edit this outfit on a VV window"
tooltipPosition="bottom-left"
onClick={() => act("vv")} />
<Button
color={!saveable && "bad"}
icon={saveable ? "save" : "trash-alt"}
tooltip={saveable
? "Save this outfit to the custom outfit list"
: "Remove this outfit from the custom outfit list"}
tooltipPosition="bottom-left"
onClick={() => act(saveable ? "save" : "delete")} />
</Stack.Item>
</Stack>
}>
<Box textAlign="center">
<Stack mb={2}>
<OutfitSlot name="Headgear" icon="hard-hat" slot="head" />
<OutfitSlot name="Glasses" icon="glasses" slot="glasses" />
<OutfitSlot name="Ears" icon="headphones-alt" slot="ears" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Neck" icon="stethoscope" slot="neck" />
<OutfitSlot name="Mask" icon="theater-masks" slot="mask" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Uniform" icon="tshirt" slot="uniform" />
<OutfitSlot name="Suit" icon="user-tie" slot="suit" />
<OutfitSlot name="Gloves" icon="mitten" slot="gloves" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Suit Storage" icon="briefcase-medical" slot="suit_store" />
<OutfitSlot name="Back" icon="shopping-bag" slot="back" />
<OutfitSlot name="ID" icon="id-card-o" slot="id" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Belt" icon="band-aid" slot="belt" />
<OutfitSlot name="Left Hand" icon="hand-paper" slot="l_hand" />
<OutfitSlot name="Right Hand" icon="hand-paper" slot="r_hand" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Shoes" icon="socks" slot="shoes" />
<OutfitSlot name="Left Pocket" icon="envelope-open-o" iconRot={180} slot="l_pocket" />
<OutfitSlot name="Right Pocket" icon="envelope-open-o" iconRot={180} slot="r_pocket" />
</Stack>
</Box>
</Section>
</Window.Content>
</Window>
);
};
const OutfitSlot = (props, context) => {
const { act, data } = useBackend(context);
const { name, icon, iconRot, slot } = props;
const { outfit } = data;
const currItem = outfit[slot];
return (
<Stack.Item grow={1} basis={0}>
<Button fluid height={2}
bold
// todo: intuitive way to clear items
onClick={e => act(e.ctrlKey ? "ctrlClick" : "click", { slot })} >
<Icon name={icon} rotation={iconRot} />
{name}
</Button>
<Box height="32px">
{currItem?.sprite && (
<>
<Box
as="img"
src={`data:image/jpeg;base64,${currItem?.sprite}`}
title={currItem?.desc}
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
<Icon
position="absolute"
name="times"
color="label"
style={{ cursor: 'pointer' }}
onClick={() => act("clear", { slot })} />
</>
)}
</Box>
<Box
color="label"
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}
title={currItem?.path}>
{currItem?.name || "Empty"}
</Box>
</Stack.Item>
);
};
@@ -0,0 +1,81 @@
import { useBackend } from '../backend';
import { Button, Section, Stack } from '../components';
import { Window } from '../layouts';
export const OutfitManager = (props, context) => {
const { act, data } = useBackend(context);
const { outfits } = data;
return (
<Window
title="Outfit Manager"
width={300}
height={300}>
<Window.Content>
<Section
fill
scrollable
title="Custom Outfit Manager"
buttons={
<>
<Button
icon="file-upload"
tooltip="Load an outfit from a file"
tooltipPosition="left"
onClick={() => act("load")} />
<Button
icon="copy"
tooltip="Copy an already existing outfit"
tooltipPosition="left"
onClick={() => act("copy")} />
<Button
icon="plus"
tooltip="Create a new outfit"
tooltipPosition="left"
onClick={() => act("new")} />
</>
}>
<Stack vertical>
{outfits?.map(outfit => (
<Stack.Item key={outfit.ref}>
<Stack>
<Stack.Item grow={1} shrink={1}
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}>
<Button
fluid
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}
content={outfit.name}
onClick={() => act("edit", { outfit: outfit.ref })} />
</Stack.Item>
<Stack.Item ml={0.5}>
<Button
icon="save"
tooltip="Save this outfit to a file"
tooltipPosition="left"
onClick={() => act("save", { outfit: outfit.ref })} />
</Stack.Item>
<Stack.Item ml={0.5}>
<Button
color="bad"
icon="trash-alt"
tooltip="Delete this outfit"
tooltipPosition="left"
onClick={() => act("delete", { outfit: outfit.ref })} />
</Stack.Item>
</Stack>
</Stack.Item>
))}
</Stack>
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,130 @@
import { useBackend, useSharedState } from '../backend';
import { Window } from '../layouts';
import { Button, Dropdown, Section, Stack } from '../components';
export const PaintingMachine = (props, context) => {
const { act, data } = useBackend(context);
const {
pdaTypes,
cardTrims,
hasPDA,
pdaName,
hasID,
idName,
} = data;
const [
selectedPDA,
] = useSharedState(context, "pdaSelection", pdaTypes[Object.keys(pdaTypes)[0]]);
const [
selectedTrim,
] = useSharedState(context, "trimSelection", cardTrims[Object.keys(cardTrims)[0]]);
return (
<Window
width={500}
height={620}>
<Window.Content scrollable>
<Section
title="PDA Painter"
buttons={
<Button.Confirm
disabled={!hasPDA}
content="Paint PDA"
confirmContent="Confirm?"
onClick={() => act("trim_pda", {
selection: selectedPDA,
})} />
}>
<Stack vertical>
<Stack.Item height="100%">
<EjectButton
name={pdaName || "-----"}
onClickEject={() => act("eject_pda")} />
</Stack.Item>
<Stack.Item height="100%">
<PainterDropdown
stateKey="pdaSelection"
options={pdaTypes} />
</Stack.Item>
</Stack>
</Section>
<Section
title="ID Trim Imprinter"
buttons={
<>
<Button.Confirm
disabled={!hasID}
content="Imprint ID Trim"
confirmContent="Confirm?"
onClick={sel => act("trim_card", {
selection: selectedTrim,
})} />
<Button
icon="question-circle"
tooltip={"WARNING: This is destructive"
+ " and will wipe ALL access on the card."}
tooltipPosition="left" />
</>
}>
<Stack vertical>
<Stack.Item height="100%">
<EjectButton
name={idName || "-----"}
onClickEject={() => act("eject_card")} />
</Stack.Item>
<Stack.Item height="100%">
<PainterDropdown
stateKey="trimSelection"
options={cardTrims} />
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
};
export const EjectButton = (props, context) => {
const {
name,
onClickEject,
} = props;
return (
<Button
fluid
ellipsis
icon="eject"
content={name}
onClick={() => onClickEject()} />
);
};
export const PainterDropdown = (props, context) => {
const {
stateKey,
options,
} = props;
const [
selectedOption,
setSelectedOption,
] = useSharedState(context, stateKey, options[Object.keys(options)[0]]);
return (
<Dropdown
width="100%"
selected={selectedOption}
options={
Object.keys(options).map(path => {
return options[path];
})
}
onSelected={sel => setSelectedOption(sel)} />
);
};
@@ -0,0 +1,270 @@
import { multiline } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Divider, Icon, NumberInput, Section, Stack } from '../components';
import { Window } from '../layouts';
const buttonWidth = 2;
const TAB2NAME = [
{
component: () => ShoppingTab,
},
{
component: () => CheckoutTab,
},
];
const ShoppingTab = (props, context) => {
const { data, act } = useBackend(context);
const {
order_datums,
} = data;
const [
shopIndex,
setShopIndex,
] = useLocalState(context, 'shop-index', 1);
const mapped_food = order_datums.filter(food => (
food && food.cat === shopIndex
));
return (
<Stack fill vertical>
<Section mb={-0.9}>
<Stack.Item>
<Stack textAlign="center">
<Stack.Item grow>
<Button
fluid
color="green"
content="Fruits and Veggies"
onClick={() => setShopIndex(1)} />
</Stack.Item>
<Stack.Item grow>
<Button
fluid
color="white"
content="Milk and Eggs"
onClick={() => setShopIndex(2)} />
</Stack.Item>
<Stack.Item grow>
<Button
fluid
color="olive"
content="Sauces and Reagents"
onClick={() => setShopIndex(3)} />
</Stack.Item>
</Stack>
</Stack.Item>
</Section>
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical mt={-2}>
<Divider />
{mapped_food.map(item => (
<Stack.Item key={item}>
<Stack>
<Stack.Item grow>
{item.name}
</Stack.Item>
<Stack.Item mt={-1} color="label" fontSize="10px">
{"\""+item.desc+"\""}
<br />
<Box textAlign="right">
{item.name+" costs "+item.cost+" per order."}
</Box>
</Stack.Item>
<Stack.Item mt={-0.5}>
<NumberInput
animated
value={item.amt && item.amt || 0}
width="41px"
minValue={0}
maxValue={20}
onChange={(e, value) => act('cart_set', {
target: item.ref,
amt: value,
})} />
</Stack.Item>
</Stack>
<Divider />
</Stack.Item>
))}
</Stack>
</Section>
</Stack.Item>
</Stack>
);
};
const CheckoutTab = (props, context) => {
const { data, act } = useBackend(context);
const {
order_datums,
total_cost,
} = data;
const checkout_list = order_datums.filter(food => (
food && food.amt
));
return (
<Stack vertical fill>
<Stack.Item grow>
<Section fill scrollable>
<Stack vertical fill>
<Stack.Item textAlign="center">
Checkout list:
</Stack.Item>
<Divider />
{!checkout_list.length && (
<>
<Box align="center" mt="15%" fontSize="40px">
Nothing!
</Box>
<br />
<Box align="center" mt={2} fontSize="15px">
(Go order something, will ya?)
</Box>
</>
)}
<Stack.Item grow>
{checkout_list.map(item => (
<Stack.Item key={item}>
<Stack>
<Stack.Item grow>
{item.name}
</Stack.Item>
<Stack.Item mt={-1} color="label" fontSize="10px">
{"\""+item.desc+"\""}
<br />
<Box textAlign="right">
{item.name+" costs "+item.cost+" per order."}
</Box>
</Stack.Item>
<Stack.Item mt={-0.5}>
<NumberInput
value={item.amt && item.amt || 0}
width="41px"
minValue={0}
maxValue={item.cost > 10 && 50 || 10}
onChange={(e, value) => act('cart_set', {
target: item.ref,
amt: value,
})} />
</Stack.Item>
</Stack>
<Divider />
</Stack.Item>
))}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item>
<Section>
<Stack>
<Stack.Item grow mt={0.5}>
Total Cost: {total_cost}
</Stack.Item>
<Stack.Item grow textAlign="center">
<Button
fluid
icon="plane-departure"
content="Purchase"
tooltip={multiline`
Your groceries will arrive at cargo,
and hopefully get delivered by them.
`}
tooltipPosition="top"
onClick={() => act('purchase')} />
</Stack.Item>
<Stack.Item grow textAlign="center">
<Button
fluid
icon="parachute-box"
color="yellow"
content="Express"
tooltip={multiline`
Sends the ingredients instantly,
and locks the console longer. Doubles the price!
`}
tooltipPosition="top-left"
onClick={() => act('express')} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
</Stack>
);
};
const OrderSent = (props, context) => {
const { act, data } = useBackend(context);
return (
<Dimmer>
<Stack vertical>
<Stack.Item>
<Icon
ml="28%"
color="green"
name="plane-arrival"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="green">
Order sent! Machine on cooldown...
</Stack.Item>
</Stack>
</Dimmer>
);
};
export const ProduceConsole = (props, context) => {
const { act, data } = useBackend(context);
const {
off_cooldown,
} = data;
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'tab-index', 1);
const TabComponent = TAB2NAME[tabIndex-1].component();
return (
<Window
title="Produce Orders"
width={500}
height={400}>
<Window.Content>
{!off_cooldown && (
<OrderSent />
)}
<Stack vertical fill>
<Stack.Item>
<Section fill>
<Stack textAlign="center">
<Stack.Item grow={3}>
<Button
fluid
color="green"
lineHeight={buttonWidth}
icon="cart-plus"
content="Shopping"
onClick={() => setTabIndex(1)} />
</Stack.Item>
<Stack.Item grow>
<Button
fluid
color="green"
lineHeight={buttonWidth}
icon="dollar-sign"
content="Checkout"
onClick={() => setTabIndex(2)} />
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<TabComponent />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
+514
View File
@@ -0,0 +1,514 @@
import { useBackend, useLocalState } from '../backend';
import { Button, Icon, LabeledList, NumberInput, Section, Stack, Table } from '../components';
import { Window } from '../layouts';
import { ReagentLookup } from './common/ReagentLookup';
import { RecipeLookup } from './common/RecipeLookup';
const bookmarkedReactions = new Set();
const matchBitflag = (a, b) => (a & b) && (a | b) === b;
export const Reagents = (props, context) => {
const { act, data } = useBackend(context);
const {
beakerSync,
reagent_mode_recipe,
reagent_mode_reagent,
bitflags = {},
} = data;
const flagIcons = [
{ flag: bitflags.BRUTE, icon: "gavel" },
{ flag: bitflags.BURN, icon: "burn" },
{ flag: bitflags.TOXIN, icon: "biohazard" },
{ flag: bitflags.OXY, icon: "wind" },
{ flag: bitflags.CLONE, icon: "male" },
{ flag: bitflags.HEALING, icon: "medkit" },
{ flag: bitflags.DAMAGING, icon: "skull-crossbones" },
{ flag: bitflags.EXPLOSIVE, icon: "bomb" },
{ flag: bitflags.OTHER, icon: "question" },
{ flag: bitflags.DANGEROUS, icon: "exclamation-triangle" },
{ flag: bitflags.EASY, icon: "chess-pawn" },
{ flag: bitflags.MODERATE, icon: "chess-knight" },
{ flag: bitflags.HARD, icon: "chess-queen" },
{ flag: bitflags.ORGAN, icon: "brain" },
{ flag: bitflags.DRINK, icon: "cocktail" },
{ flag: bitflags.FOOD, icon: "drumstick-bite" },
{ flag: bitflags.SLIME, icon: "microscope" },
{ flag: bitflags.DRUG, icon: "pills" },
{ flag: bitflags.UNIQUE, icon: "puzzle-piece" },
{ flag: bitflags.CHEMICAL, icon: "flask" },
{ flag: bitflags.PLANT, icon: "seedling" },
{ flag: bitflags.COMPETITIVE, icon: "recycle" },
];
const [page, setPage] = useLocalState(context, "page", 1);
return (
<Window
width={720}
height={850}>
<Window.Content>
<Stack fill vertical>
<Stack.Item>
<Stack fill>
<Stack.Item grow basis={0}>
<Section
title="Recipe lookup"
minWidth="353px"
buttons={(
<>
<Button
content="Beaker Sync"
icon="atom"
color={beakerSync ? "green" : "red"}
tooltip="When enabled the displayed reaction will automatically display ongoing reactions in the associated beaker."
onClick={() => act('beaker_sync')} />
<Button
content="Search"
icon="search"
color="purple"
tooltip="Search for a recipe by product name"
onClick={() => act('search_recipe')} />
<Button
icon="times"
color="red"
disabled={!reagent_mode_recipe}
onClick={() => act('recipe_click', {
id: null,
})} />
</>
)}>
<RecipeLookup
recipe={reagent_mode_recipe}
bookmarkedReactions={bookmarkedReactions} />
</Section>
</Stack.Item>
<Stack.Item grow basis={0}>
<Section
title="Reagent lookup"
minWidth="300px"
buttons={(
<>
<Button
content="Search"
icon="search"
tooltip="Search for a reagent by name"
tooltipPosition="left"
onClick={() => act('search_reagents')} />
<Button
icon="times"
color="red"
disabled={!reagent_mode_reagent}
onClick={() => act('reagent_click', {
id: null,
})} />
</>
)}>
<ReagentLookup reagent={reagent_mode_reagent} />
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Section title="Tags">
<TagBox
bitflags={bitflags} />
</Section>
</Stack.Item>
<Stack.Item grow={2} basis={0}>
<RecipeLibrary
flagIcons={flagIcons} />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
const TagBox = (props, context) => {
const { act, data } = useBackend(context);
const [page, setPage] = useLocalState(context, "page", 1);
const { bitflags } = props;
const { selectedBitflags } = data;
return (
<LabeledList>
<LabeledList.Item label="Affects">
<Button
color={selectedBitflags & bitflags.BRUTE ? "green" : "red"}
icon="gavel"
onClick={() => {
act('toggle_tag_brute');
setPage(1);
}}>
Brute
</Button>
<Button
color={selectedBitflags & bitflags.BURN ? "green" : "red"}
icon="burn"
onClick={() => {
act('toggle_tag_burn');
setPage(1);
}}>
Burn
</Button>
<Button
color={selectedBitflags & bitflags.TOXIN ? "green" : "red"}
icon="biohazard"
onClick={() => {
act('toggle_tag_toxin');
setPage(1);
}}>
Toxin
</Button>
<Button
color={selectedBitflags & bitflags.OXY ? "green" : "red"}
icon="wind"
onClick={() => {
act('toggle_tag_oxy');
setPage(1);
}}>
Suffocation
</Button>
<Button
color={selectedBitflags & bitflags.CLONE ? "green" : "red"}
icon="male"
onClick={() => {
act('toggle_tag_clone');
setPage(1);
}}>
Clone
</Button>
<Button
color={selectedBitflags & bitflags.ORGAN ? "green" : "red"}
icon="brain"
onClick={() => {
act('toggle_tag_organ');
setPage(1);
}}>
Organ
</Button>
<Button
icon="flask"
color={selectedBitflags & bitflags.CHEMICAL ? "green" : "red"}
onClick={() => {
act('toggle_tag_chemical');
setPage(1);
}}>
Chemical
</Button>
<Button
icon="seedling"
color={selectedBitflags & bitflags.PLANT ? "green" : "red"}
onClick={() => {
act('toggle_tag_plant');
setPage(1);
}}>
Plants
</Button>
<Button
icon="question"
color={selectedBitflags & bitflags.OTHER ? "green" : "red"}
onClick={() => {
act('toggle_tag_other');
setPage(1);
}}>
Other
</Button>
</LabeledList.Item>
<LabeledList.Item label="Type">
<Button
color={selectedBitflags & bitflags.DRINK ? "green" : "red"}
icon="cocktail"
onClick={() => {
act('toggle_tag_drink');
setPage(1);
}}>
Drink
</Button>
<Button
color={selectedBitflags & bitflags.FOOD ? "green" : "red"}
icon="drumstick-bite"
onClick={() => {
act('toggle_tag_food');
setPage(1);
}}>
Food
</Button>
<Button
color={selectedBitflags & bitflags.HEALING ? "green" : "red"}
icon="medkit"
onClick={() => {
act('toggle_tag_healing');
setPage(1);
}}>
Healing
</Button>
<Button
icon="skull-crossbones"
color={selectedBitflags & bitflags.DAMAGING ? "green" : "red"}
onClick={() => {
act('toggle_tag_damaging');
setPage(1);
}}>
Toxic
</Button>
<Button
icon="pills"
color={selectedBitflags & bitflags.DRUG ? "green" : "red"}
onClick={() => {
act('toggle_tag_drug');
setPage(1);
}}>
Drugs
</Button>
<Button
icon="microscope"
color={selectedBitflags & bitflags.SLIME ? "green" : "red"}
onClick={() => {
act('toggle_tag_slime');
setPage(1);
}}>
Slime
</Button>
<Button
icon="bomb"
color={selectedBitflags & bitflags.EXPLOSIVE ? "green" : "red"}
onClick={() => {
act('toggle_tag_explosive');
setPage(1);
}}>
Explosive
</Button>
<Button
icon="puzzle-piece"
color={selectedBitflags & bitflags.UNIQUE ? "green" : "red"}
onClick={() => {
act('toggle_tag_unique');
setPage(1);
}}>
Unique
</Button>
</LabeledList.Item>
<LabeledList.Item label="Difficulty">
<Button
icon="chess-pawn"
color={selectedBitflags & bitflags.EASY ? "green" : "red"}
onClick={() => {
act('toggle_tag_easy');
setPage(1);
}}>
Easy
</Button>
<Button
icon="chess-knight"
color={selectedBitflags & bitflags.MODERATE ? "green" : "red"}
onClick={() => {
act('toggle_tag_moderate');
setPage(1);
}}>
Moderate
</Button>
<Button
icon="chess-queen"
color={selectedBitflags & bitflags.HARD ? "green" : "red"}
onClick={() => {
act('toggle_tag_hard');
setPage(1);
}}>
Hard
</Button>
<Button
icon="exclamation-triangle"
color={selectedBitflags & bitflags.DANGEROUS ? "green" : "red"}
onClick={() => {
act('toggle_tag_dangerous');
setPage(1);
}}>
Dangerous
</Button>
<Button
icon="recycle"
color={selectedBitflags & bitflags.COMPETITIVE ? "green" : "red"}
onClick={() => {
act('toggle_tag_competitive');
setPage(1);
}}>
Competitive
</Button>
</LabeledList.Item>
</LabeledList>
);
};
const RecipeLibrary = (props, context) => {
const { act, data } = useBackend(context);
const [page, setPage] = useLocalState(context, "page", 1);
const { flagIcons } = props;
const {
selectedBitflags,
currentReagents = [],
master_reaction_list = [],
linkedBeaker,
} = data;
const [reagentFilter, setReagentFilter] = useLocalState(
context, 'reagentFilter', true);
const [bookmarkMode, setBookmarkMode] = useLocalState(
context, 'bookmarkMode', false);
const matchReagents = reaction => {
if (!reagentFilter || currentReagents === null) {
return true;
}
let matches = reaction.reactants
.filter(reactant => currentReagents.includes(reactant.id))
.length;
return matches === currentReagents.length;
};
const bookmarkArray = Array.from(bookmarkedReactions);
const startIndex = 50 * (page-1);
const endIndex = 50 * page;
const visibleReactions = bookmarkMode
? bookmarkArray
: master_reaction_list.filter(reaction => (
(selectedBitflags
? matchBitflag(selectedBitflags, reaction.bitflags)
: true)
&& matchReagents(reaction)
));
const pageIndexMax = Math.ceil(visibleReactions.length/50);
const addBookmark = bookmark => {
bookmarkedReactions.add(bookmark);
};
const removeBookmark = bookmark => {
bookmarkedReactions.delete(bookmark);
};
return (
<Section
fill
scrollable
title={bookmarkMode ? "Bookmarked recipes" : "Possible recipes"}
buttons={(
<>
Beaker: {linkedBeaker+" "}
<Button
content="Filter by reagents in beaker"
icon="search"
disabled={bookmarkMode}
color={reagentFilter ? "green" : "red"}
onClick={() => {
setReagentFilter(!reagentFilter);
setPage(1);
}} />
<Button
content="Bookmarks"
icon="book"
color={bookmarkMode ? "green" : "red"}
onClick={() => {
setBookmarkMode(!bookmarkMode);
setPage(1);
}} />
<Button
icon="minus"
disabled={page === 1}
onClick={() => setPage(Math.max(page - 1, 1))} />
<NumberInput
width="25px"
step={1}
stepPixelSize={3}
value={page}
minValue={1}
maxValue={pageIndexMax}
onDrag={(e, value) => setPage(value)} />
<Button
icon="plus"
disabled={page === pageIndexMax}
onClick={() => setPage(Math.min(page + 1, pageIndexMax))} />
</>
)}>
<Table>
<Table.Row>
<Table.Cell bold color="label">
Reaction
</Table.Cell>
<Table.Cell bold color="label">
Required reagents
</Table.Cell>
<Table.Cell bold color="label">
Tags
</Table.Cell>
<Table.Cell bold color="label" width="20px">
{!bookmarkMode ? "Save" : "Del"}
</Table.Cell>
</Table.Row>
{visibleReactions.slice(startIndex, endIndex).map(reaction => (
<Table.Row
key={reaction.id}
className="candystripe">
<Table.Cell bold color="label">
<Button
mt={0.5}
icon="flask"
color="purple"
content={reaction.name}
onClick={() => act('recipe_click', {
id: reaction.id,
})} />
</Table.Cell>
<Table.Cell>
{reaction.reactants.map(reactant => (
<Button
key={reactant.id}
mt={0.1}
icon="vial"
textColor="white"
color={currentReagents?.includes(reactant.id) && "green"} // check here
content={reactant.name}
onClick={() => act('reagent_click', {
id: reactant.id,
})} />
))}
</Table.Cell>
<Table.Cell width="60px">
{flagIcons
.filter(meta => reaction.bitflags & meta.flag)
.map(meta => (
<Icon
key={meta.flag}
name={meta.icon}
mr={1} />
))}
</Table.Cell>
<Table.Cell width="20px">
{!bookmarkMode && (
<Button
icon="book"
color="green"
disabled={bookmarkedReactions.has(reaction)}
onClick={() => {
addBookmark(reaction);
act('update_ui');
}} />
) || (
<Button
icon="trash"
color="red"
onClick={() => removeBookmark(reaction)} />
)}
</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
);
};
@@ -0,0 +1,217 @@
import { capitalize } from 'common/string';
import { useBackend, useSharedState } from '../backend';
import { AnimatedNumber, BlockQuote, Box, Button, Collapsible, Dimmer, Icon, LabeledList, NoticeBox, ProgressBar, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';
const ALIGNMENT2COLOR = {
"good": "yellow",
"neutral": "white",
"evil": "red",
};
export const ReligiousTool = (props, context) => {
const { act, data } = useBackend(context);
const [tab, setTab] = useSharedState(context, 'tab', 1);
const {
sects,
alignment,
toolname,
} = data;
return (
<Window
title={toolname}
width={560}
height={500}>
<Window.Content scrollable>
<Stack vertical fill>
<Stack.Item>
<Tabs textAlign="center" fluid>
<Tabs.Tab
selected={tab === 1}
onClick={() => setTab(1)}>
Sect <Icon name="place-of-worship" color={ALIGNMENT2COLOR[alignment]} />
</Tabs.Tab>
{!sects && (
<Tabs.Tab
selected={tab === 2}
onClick={() => setTab(2)}>
Rites <Icon name="pray" color={ALIGNMENT2COLOR[alignment]} />
</Tabs.Tab>
)}
</Tabs>
</Stack.Item>
<Stack.Item grow={1}>
{tab === 1 && (
!!sects && (
<SectSelectTab />
) || (
<SectTab />
)
)}
{tab === 2 && (
<RiteTab />
)}
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
const SectTab = (props, context) => {
const { act, data } = useBackend(context);
const {
name,
quote,
desc,
icon,
favordesc,
favor,
wanted,
deity,
alignment,
} = data;
return (
<Section fill>
<Stack fill vertical fontSize="15px" textAlign="center">
<Stack.Item mt={2} fontSize="32px">
<Icon name={icon} color={ALIGNMENT2COLOR[alignment]} />
{" " + name + " "}
<Icon name={icon} color={ALIGNMENT2COLOR[alignment]} />
</Stack.Item>
<Stack.Item grow mb={2} color="grey">
{"\""+quote+"\""}
</Stack.Item>
<Stack.Item color={favor === 0 ? "white" : "green"}>
{favordesc}
</Stack.Item>
<Stack.Item mb={2} textAlign="left">
<BlockQuote>
{desc}
</BlockQuote>
</Stack.Item>
<Stack.Item>
<Section mx={3} mt={-1} title="Wanted Sacrifices">
{!wanted && (
deity + " doesn't want any sacrifices."
) || (
deity + " wishes for " + wanted + "."
)}
</Section>
</Stack.Item>
</Stack>
</Section>
);
};
const SectSelectTab = (props, context) => {
const { act, data } = useBackend(context);
const {
sects,
} = data;
return (
<Section fill title="Sect Select" scrollable>
<Stack vertical>
{sects.map(sect => (
<>
<Collapsible
title={(
<Stack mt={-3.3} ml={3}>
<Stack.Item>
<Icon
name={sect.icon}
color={ALIGNMENT2COLOR[sect.alignment]} />
</Stack.Item>
<Stack.Item grow>
{sect.name}
</Stack.Item>
<Stack.Item italic >
{"\""+sect.quote+"\""}
</Stack.Item>
</Stack>
)}
color="transparent">
<Stack.Item key={sect} >
{sect.desc}<br />
<Button
mt={0.25}
textAlign="center"
icon="plus"
fluid
onClick={() => act('sect_select', {
path: sect.path,
})} >
Select {sect.name}
</Button>
</Stack.Item>
</Collapsible>
<Stack.Divider mt={-0.5} mb={0.5} />
</>
))}
</Stack>
</Section>
);
};
const RiteTab = (props, context) => {
const { act, data } = useBackend(context);
const {
rites,
deity,
icon,
alignment,
favor,
} = data;
return (
<>
{!rites.length && (
<Section fill >
<Dimmer>
<Stack vertical>
<Stack.Item textAlign="center">
<Icon
color={ALIGNMENT2COLOR[alignment]}
name={icon}
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color={ALIGNMENT2COLOR[alignment]}>
{deity} does not have any invocations.
</Stack.Item>
</Stack>
</Dimmer>
</Section>
)}
<Stack vertical>
{rites.map(rite => (
<Stack.Item key={rite}>
<Section
title={rite.name}
buttons={(
<Button
fontColor="white"
iconColor={ALIGNMENT2COLOR[alignment]}
disabled={favor < rite.favor}
color="transparent"
icon="arrow-right"
onClick={() => act('perform_rite', {
path: rite.path,
})} >
Invoke
</Button>
)} >
<Box
color={favor < rite.favor ? "red" : "grey"}
mb={0.5}>
<Icon name="star" color={ALIGNMENT2COLOR[alignment]} /> Costs {rite.favor} favor.
</Box>
<BlockQuote>
{rite.desc}
</BlockQuote>
</Section>
</Stack.Item>
))}
</Stack>
</>
);
};
@@ -0,0 +1,214 @@
import { filter, map, sortBy, uniq } from 'common/collections';
import { flow } from 'common/fp';
import { createSearch } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Icon, Input, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';
// here's an important mental define:
// custom outfits give a ref keyword instead of path
const getOutfitKey = outfit => outfit.path || outfit.ref;
const useOutfitTabs = (context, categories) => {
return useLocalState(context, 'selected-tab', categories[0]);
};
export const SelectEquipment = (props, context) => {
const { act, data } = useBackend(context);
const {
name,
icon64,
current_outfit,
favorites,
} = data;
const isFavorited = entry => favorites?.includes(entry.path);
const outfits = map(entry => ({
...entry,
favorite: isFavorited(entry),
}))([
...data.outfits,
...data.custom_outfits,
]);
// even if no custom outfits were sent, we still want to make sure there's
// at least a 'Custom' tab so the button to create a new one pops up
const categories = uniq([
...outfits.map(entry => entry.category),
'Custom',
]);
const [tab] = useOutfitTabs(context, categories);
const [searchText, setSearchText] = useLocalState(
context, 'searchText', '');
const searchFilter = createSearch(searchText, entry => (
entry.name + entry.path
));
const visibleOutfits = flow([
filter(entry => entry.category === tab),
filter(searchFilter),
sortBy(
entry => !entry.favorite,
entry => !entry.priority,
entry => entry.name
),
])(outfits);
const getOutfitEntry = current_outfit => outfits.find(outfit => (
getOutfitKey(outfit) === current_outfit
));
const currentOutfitEntry = getOutfitEntry(current_outfit);
return (
<Window
width={650}
height={415}>
<Window.Content>
<Stack fill>
<Stack.Item>
<Stack fill vertical>
<Stack.Item>
<Input
fluid
autoFocus
placeholder="Search"
value={searchText}
onInput={(e, value) => setSearchText(value)} />
</Stack.Item>
<Stack.Item>
<DisplayTabs categories={categories} />
</Stack.Item>
<Stack.Item mt={0} grow={1} basis={0}>
<OutfitDisplay
entries={visibleOutfits}
currentTab={tab} />
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow={1} basis={0}>
<Stack fill vertical>
<Stack.Item>
<Section>
<CurrentlySelectedDisplay entry={currentOutfitEntry} />
</Section>
</Stack.Item>
<Stack.Item grow={1}>
<Section
fill
title={name}
textAlign="center">
<Box
as="img"
m={0}
src={`data:image/jpeg;base64,${icon64}`}
height="100%"
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
const DisplayTabs = (props, context) => {
const { categories } = props;
const [tab, setTab] = useOutfitTabs(context, categories);
return (
<Tabs textAlign="center">
{categories.map(category => (
<Tabs.Tab
key={category}
selected={tab === category}
onClick={() => setTab(category)}>
{category}
</Tabs.Tab>
))}
</Tabs>
);
};
const OutfitDisplay = (props, context) => {
const { act, data } = useBackend(context);
const { current_outfit } = data;
const { entries, currentTab } = props;
return (
<Section fill scrollable>
{entries.map(entry => (
<Button
key={getOutfitKey(entry)}
fluid
ellipsis
icon={entry.favorite && 'star'}
iconColor="gold"
content={entry.name}
title={entry.path || entry.name}
selected={getOutfitKey(entry) === current_outfit}
onClick={() => act('preview', { path: getOutfitKey(entry) })} />
))}
{currentTab === "Custom" && (
<Button
color="transparent"
icon="plus"
fluid
onClick={() => act('customoutfit')}>
Create a custom outfit...
</Button>
)}
</Section>
);
};
const CurrentlySelectedDisplay = (props, context) => {
const { act, data } = useBackend(context);
const { current_outfit } = data;
const { entry } = props;
return (
<Stack align="center">
{entry?.path && (
<Stack.Item>
<Icon
size={1.6}
name={entry.favorite ? 'star' : 'star-o'}
color="gold"
style={{ cursor: 'pointer' }}
onClick={() => act('togglefavorite', {
path: entry.path,
})} />
</Stack.Item>
)}
<Stack.Item grow={1} basis={0}>
<Box color="label">
Currently selected:
</Box>
<Box
title={entry?.path}
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}>
{entry?.name}
</Box>
</Stack.Item>
<Stack.Item>
<Button
mr={0.8}
lineHeight={2}
color="green"
onClick={() => act('applyoutfit', {
path: current_outfit,
})}>
Confirm
</Button>
</Stack.Item>
</Stack>
);
};
+660
View File
@@ -0,0 +1,660 @@
import { multiline } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Blink, Box, Button, Dimmer, Divider, Icon, Modal, NoticeBox, ProgressBar, Section, Stack } from '../components';
import { Window } from '../layouts';
const TAB2NAME = [
{
title: 'Enscribed Name',
blurb: 'This book answers only to its owner, and of course, must have one. The permanence of the pact between a spellbook and its owner ensures such a powerful artifact cannot fall into enemy hands, or be used in ways that break the Federation\'s rules such as bartering spells.',
component: () => EnscribedName,
noScrollable: 2,
},
{
title: 'Table of Contents',
blurb: null,
component: () => TableOfContents,
},
{
title: 'Offensive',
blurb: 'Spells and items geared towards debilitating and destroying.',
},
{
title: 'Defensive',
blurb: 'Spells and items geared towards improving your survivability or reducing foes\' ability to attack.',
},
{
title: 'Mobility',
blurb: 'Spells and items geared towards improving your ability to move. It is a good idea to take at least one.',
},
{
title: 'Assistance',
blurb: 'Spells and items geared towards bringing in outside forces to aid you or improving upon your other items and abilities.',
},
{
title: 'Challenges',
blurb: 'The Wizard Federation is looking for shows of power. Arming the station against you will increase the danger, but will grant you more charges for your spellbook.',
locked: true,
noScrollable: 1,
},
{
title: 'Rituals',
blurb: 'These powerful spells change the very fabric of reality. Not always in your favour.',
},
{
title: 'Loadouts',
blurb: 'The Wizard Federation accepts that sometimes, choosing is hard. You can choose from some approved wizard loadouts here.',
component: () => Loadouts,
noScrollable: 2,
},
{
title: 'Randomize',
blurb: 'If you didn\'t like the loadouts offered, you can embrace chaos. Not recommended for newer wizards.',
component: () => Randomize,
},
];
const BUYWORD2ICON = {
Learn: 'plus',
Summon: 'hat-wizard',
Cast: 'meteor',
};
const EnscribedName = (props, context) => {
const { act, data } = useBackend(context);
const { owner } = data;
return (
<>
<Box
mt={25}
mb={-3}
fontSize="50px"
color="bad"
textAlign="center"
fontFamily="Ink Free">
{owner}
</Box>
<Divider />
</>
);
};
const lineHeightToc = "34.6px";
const TableOfContents = (props, context) => {
const { act, data } = useBackend(context);
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'tab-index', 1);
return (
<Box textAlign="center">
<Button
lineHeight={lineHeightToc}
fluid
icon="pen"
disabled
content="Name Enscription" />
<Button
lineHeight={lineHeightToc}
fluid
icon="clipboard"
disabled
content="Table of Contents" />
<Divider />
<Button
lineHeight={lineHeightToc}
fluid
icon="fire"
content="Deadly Evocations"
onClick={() => setTabIndex(3)} />
<Button
lineHeight={lineHeightToc}
fluid
icon="shield-alt"
content="Defensive Evocations"
onClick={() => setTabIndex(3)} />
<Divider />
<Button
lineHeight={lineHeightToc}
fluid
icon="globe-americas"
content="Magical Transportation"
onClick={() => setTabIndex(5)} />
<Button
lineHeight={lineHeightToc}
fluid
icon="users"
content="Assistance and Summoning"
onClick={() => setTabIndex(5)} />
<Divider />
<Button
lineHeight={lineHeightToc}
fluid
icon="crown"
content="Challenges"
onClick={() => setTabIndex(7)} />
<Button
lineHeight={lineHeightToc}
fluid
icon="magic"
content="Rituals"
onClick={() => setTabIndex(7)} />
<Divider />
<Button
lineHeight={lineHeightToc}
fluid
icon="thumbs-up"
content="Wizard Approved Loadouts"
onClick={() => setTabIndex(9)} />
<Button
lineHeight={lineHeightToc}
fluid
icon="dice"
content="Arcane Randomizer"
onClick={() => setTabIndex(9)} />
</Box>
);
};
const LockedPage = (props, context) => {
const { act, data } = useBackend(context);
const { owner } = data;
return (
<Dimmer>
<Stack vertical>
<Stack.Item>
<Icon
color="purple"
name="lock"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="purple">
The Wizard Federation has locked this page.
</Stack.Item>
</Stack>
</Dimmer>
);
};
const PointLocked = (props, context) => {
const { act, data } = useBackend(context);
const { owner } = data;
return (
<Dimmer>
<Stack vertical>
<Stack.Item>
<Icon
color="purple"
name="dollar-sign"
size={10}
/>
<div
style={{
background: "purple",
bottom: "60%",
left: "33%",
height: "10px",
position: "relative",
transform: "rotate(45deg)",
width: "150px",
}}
/>
</Stack.Item>
<Stack.Item fontSize="18px" color="purple">
You do not have enough points to use this page.
</Stack.Item>
</Stack>
</Dimmer>
);
};
const SingleLoadout = (props, context) => {
const { act } = useBackend(context);
const { author, name, blurb, icon, loadoutId, loadoutColor } = props;
return (
<Stack.Item grow>
<Section width={LoadoutWidth} title={name}>
{blurb}
<Divider />
<Button.Confirm
confirmContent="Confirm Purchase?"
confirmIcon="dollar-sign"
confirmColor="good"
fluid
icon={icon}
content="Purchase Loadout"
onClick={() => act(loadoutId)} />
<Divider />
<Box color={loadoutColor}>
Added by {author}.
</Box>
</Section>
</Stack.Item>
);
};
const LoadoutWidth = 19.17;
const Loadouts = (props, context) => {
const { act, data } = useBackend(context);
const { points } = data;
return (
<Stack ml={0.5} mt={-0.5} vertical fill>
{points < 10 && (
<PointLocked />
)}
<Stack.Item>
<Stack fill>
<SingleLoadout
loadoutId="loadout_classic"
loadoutColor="purple"
name="The Classic Wizard"
icon="fire"
author="Archchancellor Gray"
blurb={multiline`
This is the classic wizard, crazy popular in
the 2550's. Comes with Fireball, Magic Missile,
Ei Nath, and Ethereal Jaunt. The key here is that
every part of this kit is very easy to pick up and use.
`} />
<SingleLoadout
name="Mjolnir's Power"
icon="hammer"
loadoutId="loadout_hammer"
loadoutColor="green"
author="Jegudiel Worldshaker"
blurb={multiline`
The power of the mighty Mjolnir! Best not to lose it.
This loadout has Summon Item, Mutate, Blink, and
Force Wall. Mutate is your utility in this case:
Use it for limited ranged fire and getting out of bad blinks.
`} />
</Stack>
</Stack.Item>
<Stack.Item>
<Stack fill>
<SingleLoadout
name="Fantastical Army"
icon="pastafarianism"
loadoutId="loadout_army"
loadoutColor="yellow"
author="Prospero Spellstone"
blurb={multiline`
Why kill when others will gladly do it for you?
Embrace chaos with your kit: Soulshards, Staff of Change,
Necro Stone, Teleport, and Jaunt! Remember, no offense spells!
`} />
<SingleLoadout
name="Soul Tapper"
icon="skull"
loadoutId="loadout_tap"
loadoutColor="white"
author="Tom the Empty"
blurb={multiline`
Embrace the dark, and tap into your soul.
You can recharge very long recharge spells
like Ei Nath by jumping into new bodies with
Mind Swap and starting Soul Tap anew.
`} />
</Stack>
</Stack.Item>
</Stack>
);
};
const lineHeightRandomize = 6;
const Randomize = (props, context) => {
const { act, data } = useBackend(context);
const { points } = data;
return (
<Stack fill vertical>
{points < 10 && (
<PointLocked />
)}
<Stack.Item grow mt={10}>
Semi-Randomize will ensure you at least get some mobility and lethality.
</Stack.Item>
<Stack.Item>
<Button.Confirm
confirmContent="Cowabunga it is?"
confirmIcon="dice-three"
lineHeight={lineHeightRandomize}
fluid
icon="dice-three"
content="Semi-Randomize!"
onClick={() => act("semirandomize")} />
<Divider />
</Stack.Item>
<Stack.Item>
Full Random will give you anything. There&apos;s no going back, either!
</Stack.Item>
<Stack.Item>
<NoticeBox danger>
<Button.Confirm
confirmContent="Cowabunga it is?"
confirmIcon="dice"
lineHeight={lineHeightRandomize}
fluid
color="black"
icon="dice"
content="Full Random!"
onClick={() => act("randomize")} />
</NoticeBox>
</Stack.Item>
</Stack>
);
};
const widthSection = "466px";
const heightSection = "456px";
export const Spellbook = (props, context) => {
const { act, data } = useBackend(context);
const {
entries,
points,
} = data;
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'tab-index', 1);
const ScrollableCheck = TAB2NAME[tabIndex-1].noScrollable ? false : true;
const ScrollableNextCheck = TAB2NAME[tabIndex-1].noScrollable !== 2;
const TabComponent = TAB2NAME[tabIndex-1].component
? TAB2NAME[tabIndex-1].component() : null;
const TabNextComponent = TAB2NAME[tabIndex].component
? TAB2NAME[tabIndex].component() : null;
const TabSpells = entries ? entries.filter(
entry => entry.cat === TAB2NAME[tabIndex-1].title) : null;
const TabNextSpells = entries ? entries.filter(
entry => entry.cat === TAB2NAME[tabIndex].title) : null;
return (
<Window
title="Spellbook"
theme="wizard"
width={950}
height={540}>
<Window.Content>
<Stack vertical fill>
<Stack.Item>
<Stack fill>
<Stack.Item grow>
<Section
scrollable={ScrollableCheck}
textAlign="center"
width={widthSection}
height={heightSection}
fill
title={TAB2NAME[tabIndex-1].title}
buttons={
<>
<Button
mr={57}
disabled={tabIndex === 1}
icon="arrow-left"
content="Previous Page"
onClick={() => setTabIndex(tabIndex-2)} />
<Box textAlign="right" bold mt={-3.3} mr={1}>
{tabIndex}
</Box>
</>
}>
{!!TAB2NAME[tabIndex-1].locked && (
<LockedPage />
)}
<Stack vertical>
{TAB2NAME[tabIndex-1].blurb !== null && (
<Stack.Item>
<Box
textAlign="center"
bold
height="30px">
{TAB2NAME[tabIndex-1].blurb}
</Box>
</Stack.Item>
)}
{!!TAB2NAME[tabIndex-1].component && (
<Stack.Item>
<TabComponent />
</Stack.Item>
) || (
<Stack.Item>
<Stack vertical>
{TabSpells?.map(entry => (
<Stack.Item key={entry}>
<Divider />
<Section
title={entry.name}
buttons={
<>
<Box mr={entry.buyword === "Learn" ? 6.5 : 2}>
{entry.cost} Points
</Box>
{entry.cat === 'Rituals' && (
!!entry.times && (
<Box ml={-104} mt={-2.2}>
Cast {entry.times} Times.
</Box>
) || (
<Box ml={-110} mt={-2.2}>
Not Casted Yet.
</Box>
)
) || (
entry.cooldown && (
<Box ml={-115} mt={-2.2}>
{entry.cooldown}s Cooldown
</Box>
) || (
<Box ml={-120} mt={-2.2}>
No Cooldown!
</Box>)
)}
{entry.buyword === "Learn" && (
<Box mr={-9.5} mt={-3}>
<Button
icon="tshirt"
color={entry.clothes_req ? "bad" : "green"}
tooltipPosition="bottom-left"
tooltip={entry.clothes_req
? "Requires wizard garb."
:"Can be cast without wizard garb."} />
</Box>
)}
</>
}>
<Stack>
<Stack.Item grow>
{entry.desc}
</Stack.Item>
<Stack.Item>
<Divider vertical />
</Stack.Item>
<Stack.Item>
<Button
fluid
textAlign="center"
color={points >= entry.cost ? "green" : "bad"}
disabled={points < entry.cost}
width={7}
icon={BUYWORD2ICON[entry.buyword]}
content={entry.buyword}
onClick={() => act("purchase", {
spellref: entry.ref,
})} />
<br />
{!entry.refundable && (
<NoticeBox>
No refunds.
</NoticeBox>
) || (
<Button
textAlign="center"
width={7}
icon="arrow-left"
content="Refund"
onClick={() => act("refund", {
spellref: entry.ref,
})} />
)}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
))}
</Stack>
</Stack.Item>
)}
</Stack>
</Section>
</Stack.Item>
<Stack.Item grow>
<Section
scrollable={ScrollableNextCheck}
textAlign="center"
width={widthSection}
height={heightSection}
fill
title={TAB2NAME[tabIndex].title}
buttons={
<>
<Button
mr={0}
icon="arrow-right"
disabled={tabIndex === 9}
content="Next Page"
onClick={() => setTabIndex(tabIndex+2)} />
<Box textAlign="left" bold mt={-3.3} ml={-59.8} >
{tabIndex+1}
</Box>
</>
}>
{!!TAB2NAME[tabIndex].locked && (
<LockedPage />
)}
<Stack vertical>
{TAB2NAME[tabIndex].blurb !== null && (
<Stack.Item>
<Box
textAlign="center"
bold
height="30px">
{TAB2NAME[tabIndex].blurb}
</Box>
</Stack.Item>
)}
{!!TAB2NAME[tabIndex].component && (
<Stack.Item>
<TabNextComponent />
</Stack.Item>
) || (
<Stack.Item>
<Stack vertical>
{TabNextSpells?.map(entry => (
<Stack.Item key={entry}>
<Divider />
<Section
title={entry.name}
buttons={
<>
<Box mr={entry.buyword === "Learn" ? 6.5 : 2}>
{entry.cost} Points
</Box>
{entry.cat === 'Rituals' && (
!!entry.times && (
<Box ml={-118} mt={-2.2}>
Cast {entry.times} Time(s).
</Box>
) || (
<Box ml={-118} mt={-2.2}>
Not Casted Yet.
</Box>
)
) || (
entry.cooldown && (
<Box ml={-115} mt={-2.2}>
{entry.cooldown}s Cooldown
</Box>
) || (
<Box ml={-120} mt={-2.2}>
No Cooldown!
</Box>
)
)}
{entry.buyword === "Learn" && (
<Box mr={-9.5} mt={-3}>
<Button
icon="tshirt"
color={entry.clothes_req ? "bad" : "green"}
tooltipPosition="bottom-left"
tooltip={entry.clothes_req
? "Requires wizard garb."
:"Can be cast without wizard garb."} />
</Box>
)}
</>
}>
<Stack>
<Stack.Item grow>
{entry.desc}
</Stack.Item>
<Stack.Item>
<Divider vertical />
</Stack.Item>
<Stack.Item>
<Button
fluid
textAlign="center"
color={points >= entry.cost ? "green" : "bad"}
disabled={points < entry.cost}
width={7}
icon={BUYWORD2ICON[entry.buyword]}
content={entry.buyword}
onClick={() => act("purchase", {
spellref: entry.ref,
})} />
<br />
{!entry.refundable && (
<NoticeBox>
No refunds.
</NoticeBox>
) || (
<Button
textAlign="center"
width={7}
icon="arrow-left"
content="Refund"
onClick={() => act("refund", {
spellref: entry.ref,
})} />
)}
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
))}
</Stack>
</Stack.Item>
)}
</Stack>
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Section>
<ProgressBar
value={points/10}>
{points + ' points left to spend.'}
</ProgressBar>
</Section>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
+418
View File
@@ -0,0 +1,418 @@
import { range } from "common/collections";
import { BooleanLike } from "common/react";
import { resolveAsset } from "../assets";
import { useBackend } from "../backend";
import { Box, Button, Icon, Stack } from "../components";
import { Window } from "../layouts";
const ROWS = 5;
const COLUMNS = 6;
const BUTTON_DIMENSIONS = "50px";
type GridSpotKey = string;
const getGridSpotKey = (spot: [number, number]): GridSpotKey => {
return `${spot[0]}/${spot[1]}`;
};
const CornerText = (props: {
align: "left" | "right";
children: string;
}): JSX.Element => {
const { align, children } = props;
return (
<Box
style={{
position: "relative",
left: align === "left" ? "2px" : "-2px",
"text-align": align,
"text-shadow": "1px 1px 1px #555",
}}
>
{children}
</Box>
);
};
type AlternateAction = {
icon: string;
text: string;
};
const ALTERNATE_ACTIONS: Record<string, AlternateAction> = {
knot: {
icon: "shoe-prints",
text: "Knot",
},
untie: {
icon: "shoe-prints",
text: "Untie",
},
unknot: {
icon: "shoe-prints",
text: "Unknot",
},
enable_internals: {
icon: "tg-air-tank",
text: "Enable internals",
},
disable_internals: {
icon: "tg-air-tank-slash",
text: "Disable internals",
},
adjust_jumpsuit: {
icon: "tshirt",
text: "Adjust jumpsuit",
},
};
const SLOTS: Record<
string,
{
displayName: string;
gridSpot: GridSpotKey;
image?: string;
additionalComponent?: JSX.Element;
}
> = {
eyes: {
displayName: "eyewear",
gridSpot: getGridSpotKey([0, 1]),
image: "inventory-glasses.png",
},
head: {
displayName: "headwear",
gridSpot: getGridSpotKey([0, 2]),
image: "inventory-head.png",
},
neck: {
displayName: "neckwear",
gridSpot: getGridSpotKey([1, 1]),
image: "inventory-neck.png",
},
mask: {
displayName: "mask",
gridSpot: getGridSpotKey([1, 2]),
image: "inventory-mask.png",
},
corgi_collar: {
displayName: "collar",
gridSpot: getGridSpotKey([1, 2]),
image: "inventory-collar.png",
},
ears: {
displayName: "earwear",
gridSpot: getGridSpotKey([1, 3]),
image: "inventory-ears.png",
},
parrot_headset: {
displayName: "headset",
gridSpot: getGridSpotKey([1, 3]),
image: "inventory-ears.png",
},
handcuffs: {
displayName: "handcuffs",
gridSpot: getGridSpotKey([1, 4]),
},
legcuffs: {
displayName: "legcuffs",
gridSpot: getGridSpotKey([1, 5]),
},
jumpsuit: {
displayName: "uniform",
gridSpot: getGridSpotKey([2, 1]),
image: "inventory-uniform.png",
},
suit: {
displayName: "suit",
gridSpot: getGridSpotKey([2, 2]),
image: "inventory-suit.png",
},
gloves: {
displayName: "gloves",
gridSpot: getGridSpotKey([2, 3]),
image: "inventory-gloves.png",
},
right_hand: {
displayName: "right hand",
gridSpot: getGridSpotKey([2, 4]),
image: "inventory-hand_r.png",
additionalComponent: <CornerText align="left">R</CornerText>,
},
left_hand: {
displayName: "left hand",
gridSpot: getGridSpotKey([2, 5]),
image: "inventory-hand_l.png",
additionalComponent: <CornerText align="right">L</CornerText>,
},
shoes: {
displayName: "shoes",
gridSpot: getGridSpotKey([3, 2]),
image: "inventory-shoes.png",
},
suit_storage: {
displayName: "suit storage item",
gridSpot: getGridSpotKey([4, 0]),
image: "inventory-suit_storage.png",
},
id: {
displayName: "ID",
gridSpot: getGridSpotKey([4, 1]),
image: "inventory-id.png",
},
belt: {
displayName: "belt",
gridSpot: getGridSpotKey([4, 2]),
image: "inventory-belt.png",
},
back: {
displayName: "backpack",
gridSpot: getGridSpotKey([4, 3]),
image: "inventory-back.png",
},
left_pocket: {
displayName: "left pocket",
gridSpot: getGridSpotKey([4, 4]),
image: "inventory-pocket.png",
},
right_pocket: {
displayName: "right pocket",
gridSpot: getGridSpotKey([4, 5]),
image: "inventory-pocket.png",
},
};
enum ObscuringLevel {
Completely = 1,
Hidden = 2,
}
type Interactable = {
interacting: BooleanLike;
};
/**
* Some possible options:
*
* null - No interactions, no item, but is an available slot
* { interacting: 1 } - No item, but we're interacting with it
* { icon: icon, name: name } - An item with no alternate actions
* that we're not interacting with.
* { icon, name, interacting: 1 } - An item with no alternate actions
* that we're interacting with.
*/
type StripMenuItem =
| null
| Interactable
| ((
| {
icon: string;
name: string;
alternate?: string;
}
| {
obscured: ObscuringLevel;
}
) &
Partial<Interactable>);
type StripMenuData = {
items: Record<keyof typeof SLOTS, StripMenuItem>;
name: string;
};
export const StripMenu = (props, context) => {
const { act, data } = useBackend<StripMenuData>(context);
const gridSpots = new Map<GridSpotKey, string>();
for (const key of Object.keys(data.items)) {
gridSpots.set(SLOTS[key].gridSpot, key);
}
return (
<Window title={`Stripping ${data.name}`} width={400} height={400}>
<Window.Content>
<Stack fill vertical>
{range(0, ROWS).map(row => (
<Stack.Item key={row}>
<Stack fill>
{range(0, COLUMNS).map(column => {
const key = getGridSpotKey([row, column]);
const keyAtSpot = gridSpots.get(key);
if (!keyAtSpot) {
return (
<Stack.Item
key={key}
style={{
width: BUTTON_DIMENSIONS,
height: BUTTON_DIMENSIONS,
}}
/>
);
}
const item = data.items[keyAtSpot];
const slot = SLOTS[keyAtSpot];
let alternateAction: AlternateAction | undefined;
let content;
let tooltip;
if (item === null) {
tooltip = slot.displayName;
} else if ("name" in item) {
alternateAction = ALTERNATE_ACTIONS[item.alternate];
content = (
<Box
as="img"
src={`data:image/jpeg;base64,${item.icon}`}
height="100%"
width="100%"
style={{
"-ms-interpolation-mode": "nearest-neighbor",
"vertical-align": "middle",
}}
/>
);
tooltip = item.name;
} else if ("obscured" in item) {
content = (
<Icon
name={
item.obscured === ObscuringLevel.Completely
? "ban"
: "eye-slash"
}
size={3}
ml={0}
mt={1.3}
style={{
"text-align": "center",
height: "100%",
width: "100%",
}}
/>
);
tooltip = `obscured ${slot.displayName}`;
}
return (
<Stack.Item
key={key}
style={{
width: BUTTON_DIMENSIONS,
height: BUTTON_DIMENSIONS,
}}
>
<Box
style={{
position: "relative",
width: "100%",
height: "100%",
}}
>
<Button
onClick={() => {
act("use", {
key: keyAtSpot,
});
}}
fluid
tooltip={tooltip}
style={{
background: item?.interacting
? "hsl(39, 73%, 30%)"
: undefined,
position: "relative",
width: "100%",
height: "100%",
padding: 0,
}}
>
{slot.image && (
<Box
as="img"
src={resolveAsset(slot.image)}
opacity={0.7}
style={{
position: "absolute",
width: "32px",
height: "32px",
left: "50%",
top: "50%",
transform:
"translateX(-50%) translateY(-50%) scale(0.8)",
}}
/>
)}
<Box style={{ position: "relative" }}>
{content}
</Box>
{slot.additionalComponent}
</Button>
{alternateAction !== undefined && (
<Button
onClick={() => {
act("alt", {
key: keyAtSpot,
});
}}
tooltip={alternateAction.text}
style={{
background: "rgba(0, 0, 0, 0.6)",
position: "absolute",
bottom: 0,
right: 0,
"z-index": 2,
}}
>
<Icon name={alternateAction.icon} />
</Button>
)}
</Box>
</Stack.Item>
);
})}
</Stack>
</Stack.Item>
))}
</Stack>
</Window.Content>
</Window>
);
};
+796
View File
@@ -0,0 +1,796 @@
import { filter, map, sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { useBackend, useLocalState } from '../backend';
import { Button, Section, Modal, Dropdown, Tabs, Box, Input, Flex, ProgressBar, Collapsible, Icon, Divider } from '../components';
import { Window, NtosWindow } from '../layouts';
import { Experiment } from './ExperimentConfigure';
// Data reshaping / ingestion (thanks stylemistake for the help, very cool!)
// This is primarily necessary due to measures that are taken to reduce the size
// of the sent static JSON payload to as minimal of a size as possible
// as larger sizes cause a delay for the user when opening the UI.
const remappingIdCache = {};
const remapId = id => remappingIdCache[id];
const selectRemappedStaticData = data => {
// Handle reshaping of node cache to fill in unsent fields, and
// decompress the node IDs
const node_cache = {};
for (let id of Object.keys(data.static_data.node_cache)) {
const node = data.static_data.node_cache[id];
const costs = Object.keys(node.costs || {}).map(x => ({
type: remapId(x),
value: node.costs[x],
}));
node_cache[remapId(id)] = {
...node,
id: remapId(id),
costs,
prereq_ids: map(remapId)(node.prereq_ids || []),
design_ids: map(remapId)(node.design_ids || []),
unlock_ids: map(remapId)(node.unlock_ids || []),
required_experiments: node.required_experiments || [],
discount_experiments: node.discount_experiments || [],
};
}
// Do the same as the above for the design cache
const design_cache = {};
for (let id of Object.keys(data.static_data.design_cache)) {
const [name, classes] = data.static_data.design_cache[id];
design_cache[remapId(id)] = {
name: name,
class: classes.startsWith("design") ? classes : `design32x32 ${classes}`,
};
}
return {
node_cache,
design_cache,
};
};
let remappedStaticData;
const useRemappedBackend = context => {
const { data, ...rest } = useBackend(context);
// Only remap the static data once, cache for future use
if (!remappedStaticData) {
const id_cache = data.static_data.id_cache;
for (let i = 0; i < id_cache.length; i++) {
remappingIdCache[i + 1] = id_cache[i];
}
remappedStaticData = selectRemappedStaticData(data);
}
return {
data: {
...data,
...remappedStaticData,
},
...rest,
};
};
// Utility Functions
const abbreviations = {
"General Research": "Gen. Res.",
"Nanite Research": "Nanite Res.",
};
const abbreviateName = name => abbreviations[name] ?? name;
// Actual Components
export const Techweb = (props, context) => {
const { act, data } = useRemappedBackend(context);
const {
locked,
} = data;
return (
<Window
width={640}
height={735}>
<Window.Content scrollable>
{!!locked && (
<Modal width="15em" align="center" className="Techweb__LockedModal">
<div><b>Console Locked</b></div>
<Button
icon="unlock"
onClick={() => act("toggleLock")}>
Unlock
</Button>
</Modal>
)}
<TechwebContent />
</Window.Content>
</Window>
);
};
export const AppTechweb = (props, context) => {
const { act, data } = useRemappedBackend(context);
const {
locked,
} = data;
return (
<NtosWindow
width={640}
height={735}>
<NtosWindow.Content scrollable>
{!!locked && (
<Modal width="15em" align="center" className="Techweb__LockedModal">
<div><b>Console Locked</b></div>
<Button
icon="unlock"
onClick={() => act("toggleLock")}>
Unlock
</Button>
</Modal>
)}
<TechwebContent />
</NtosWindow.Content>
</NtosWindow>
);
};
export const TechwebContent = (props, context) => {
const { act, data } = useRemappedBackend(context);
const {
points,
points_last_tick,
web_org,
sec_protocols,
t_disk,
d_disk,
locked,
} = data;
const [
techwebRoute,
setTechwebRoute,
] = useLocalState(context, 'techwebRoute', null);
const [
lastPoints,
setLastPoints,
] = useLocalState(context, 'lastPoints', {});
return (
<Flex direction="column" className="Techweb__Viewport" height="100%">
<Flex.Item className="Techweb__HeaderSection">
<Flex className="Techweb__HeaderContent">
<Flex.Item>
<Box>
Available points:
<ul className="Techweb__PointSummary">
{Object.keys(points).map(k => (
<li key={k}>
<b>{k}</b>: {points[k]}
{!!points_last_tick[k] && (
` (+${points_last_tick[k]}/sec)`
)}
</li>
))}
</ul>
</Box>
<Box>
Security protocols:
<span
className={`Techweb__SecProtocol ${!!sec_protocols && "engaged"}`}>
{sec_protocols ? "Engaged" : "Disengaged"}
</span>
</Box>
</Flex.Item>
<Flex.Item grow={1} />
<Flex.Item>
<Button fluid
onClick={() => act("toggleLock")}
icon="lock">
Lock Console
</Button>
{d_disk && (
<Flex.Item>
<Button fluid
onClick={() => setTechwebRoute({ route: "disk", diskType: "design" })}>
Design Disk Inserted
</Button>
</Flex.Item>
)}
{t_disk && (
<Flex.Item>
<Button fluid
onClick={() => setTechwebRoute({ route: "disk", diskType: "tech" })}>
Tech Disk Inserted
</Button>
</Flex.Item>
)}
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item className="Techweb__RouterContent" height="100%">
<TechwebRouter />
</Flex.Item>
</Flex>
);
};
const TechwebRouter = (props, context) => {
const [
techwebRoute,
] = useLocalState(context, 'techwebRoute', null);
const route = techwebRoute?.route;
const RoutedComponent = (
route === "details" && TechwebNodeDetail
|| route === "disk" && TechwebDiskMenu
|| TechwebOverview
);
return (
<RoutedComponent {...techwebRoute} />
);
};
const TechwebOverview = (props, context) => {
const { act, data } = useRemappedBackend(context);
const { nodes, node_cache, design_cache } = data;
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'overviewTabIndex', 1);
const [
searchText,
setSearchText,
] = useLocalState(context, 'searchText');
// Only search when 3 or more characters have been input
const searching = searchText && searchText.trim().length > 1;
let displayedNodes = nodes;
if (searching) {
displayedNodes = displayedNodes.filter(x => {
const n = node_cache[x.id];
return n.name.toLowerCase().includes(searchText)
|| n.description.toLowerCase().includes(searchText)
|| n.design_ids.some(e =>
design_cache[e].name.toLowerCase().includes(searchText));
});
} else {
displayedNodes = sortBy(x => node_cache[x.id].name)(tabIndex < 2
? nodes.filter(x => x.tier === tabIndex)
: nodes.filter(x => x.tier >= tabIndex));
}
const switchTab = tab => {
setTabIndex(tab);
setSearchText(null);
};
return (
<Flex direction="column" height="100%">
<Flex.Item>
<Flex justify="space-between" className="Techweb__HeaderSectionTabs">
<Flex.Item align="center" className="Techweb__HeaderTabTitle">
Web View
</Flex.Item>
<Flex.Item grow={1}>
<Tabs>
<Tabs.Tab
selected={!searching && tabIndex === 0}
onClick={() => switchTab(0)}>
Researched
</Tabs.Tab>
<Tabs.Tab
selected={!searching && tabIndex === 1}
onClick={() => switchTab(1)}>
Available
</Tabs.Tab>
<Tabs.Tab
selected={!searching && tabIndex === 2}
onClick={() => switchTab(2)}>
Future
</Tabs.Tab>
{!!searching && (
<Tabs.Tab
selected>
Search Results
</Tabs.Tab>
)}
</Tabs>
</Flex.Item>
<Flex.Item align={"center"}>
<Input
value={searchText}
onInput={(e, value) => setSearchText(value)}
placeholder={"Search..."} />
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item className={"Techweb__OverviewNodes"} height="100%">
{displayedNodes.map(n => {
return (
<TechNode node={n} key={n.id} />
);
})}
</Flex.Item>
</Flex>
);
};
const TechwebNodeDetail = (props, context) => {
const { act, data } = useRemappedBackend(context);
const { nodes } = data;
const { selectedNode } = props;
const selectedNodeData = selectedNode
&& nodes.find(x => x.id === selectedNode);
return (
<TechNodeDetail node={selectedNodeData} />
);
};
const TechwebDiskMenu = (props, context) => {
const { act, data } = useRemappedBackend(context);
const { diskType } = props;
const { t_disk, d_disk } = data;
const [
techwebRoute,
setTechwebRoute,
] = useLocalState(context, 'techwebRoute', null);
// Check for the disk actually being inserted
if ((diskType === "design" && !d_disk) || (diskType === "tech" && !t_disk)) {
return null;
}
const DiskContent = diskType === "design" && TechwebDesignDisk
|| TechwebTechDisk;
return (
<Flex direction="column" height="100%">
<Flex.Item>
<Flex justify="space-between" className="Techweb__HeaderSectionTabs">
<Flex.Item align="center" className="Techweb__HeaderTabTitle">
{diskType.charAt(0).toUpperCase() + diskType.slice(1)} Disk
</Flex.Item>
<Flex.Item grow={1}>
<Tabs>
<Tabs.Tab selected>
Stored Data
</Tabs.Tab>
</Tabs>
</Flex.Item>
<Flex.Item align="center">
{diskType === "tech" && (
<Button
icon="save"
onClick={() => act("loadTech")}>
Web &rarr; Disk
</Button>
)}
<Button
icon="upload"
onClick={() => act("uploadDisk", { type: diskType })}>
Disk &rarr; Web
</Button>
<Button
icon="trash"
onClick={() => act("eraseDisk", { type: diskType })}>
Erase
</Button>
<Button
icon="eject"
onClick={() => {
act("ejectDisk", { type: diskType });
setTechwebRoute(null);
}}>
Eject
</Button>
<Button
icon="home"
onClick={() => setTechwebRoute(null)}>
Home
</Button>
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item grow={1} className="Techweb__OverviewNodes">
<DiskContent />
</Flex.Item>
</Flex>
);
};
const TechwebDesignDisk = (props, context) => {
const { act, data } = useRemappedBackend(context);
const {
design_cache,
researched_designs,
d_disk,
} = data;
const { blueprints } = d_disk;
const [
selectedDesign,
setSelectedDesign,
] = useLocalState(context, "designDiskSelect", null);
const [
showModal,
setShowModal,
] = useLocalState(context, 'showDesignModal', -1);
const designIdByIdx = Object.keys(researched_designs);
const designOptions = flow([
filter(x => x.toLowerCase() !== "error"),
map((id, idx) => `${design_cache[id].name} [${idx}]`),
sortBy(x => x),
])(designIdByIdx);
return (
<>
{showModal >= 0 && (
<Modal width="20em">
<Flex direction="column" className="Techweb__DesignModal">
<Flex.Item>
Select a design to save...
</Flex.Item>
<Flex.Item>
<Dropdown
width="100%"
options={designOptions}
onSelected={val => {
const idx = parseInt(val.split('[').pop().split(']')[0], 10);
setSelectedDesign(designIdByIdx[idx]);
}} />
</Flex.Item>
<Flex.Item align="center">
<Button
onClick={() => setShowModal(-1)}>
Cancel
</Button>
<Button
disabled={selectedDesign === null}
onClick={() => {
act("writeDesign", {
slot: showModal + 1,
selectedDesign: selectedDesign,
});
setShowModal(-1);
setSelectedDesign(null);
}}>
Select
</Button>
</Flex.Item>
</Flex>
</Modal>
)}
{blueprints.map((x, i) => (
<Section
key={i}
title={`Slot ${i + 1}`}
buttons={
<>
{x !== null && (
<Button
icon="upload"
onClick={() => act("uploadDesignSlot", { slot: i + 1 })}>
Upload Design to Web
</Button>
)}
<Button
icon="save"
onClick={() => setShowModal(i)}>
{x !== null ? "Overwrite Slot" : "Load Design to Slot"}
</Button>
{x !== null && (
<Button
icon="trash"
onClick={() => act("clearDesignSlot", { slot: i + 1 })}>
Clear Slot
</Button>
)}
</>
}>
{x === null && 'Empty' || (
<>
Contains the design for <b>{design_cache[x].name}</b>:<br />
<span
className={`${design_cache[x].class} Techweb__DesignIcon`} />
</>
)}
</Section>
))}
</>
);
};
const TechwebTechDisk = (props, context) => {
const { act, data } = useRemappedBackend(context);
const { t_disk } = data;
const { stored_research } = t_disk;
return Object.keys(stored_research).map(x => ({ id: x })).map(n => (
<TechNode key={n.id} nocontrols node={n} />
));
};
const TechNodeDetail = (props, context) => {
const { act, data } = useRemappedBackend(context);
const {
nodes,
node_cache,
} = data;
const { node } = props;
const { id } = node;
const { prereq_ids, unlock_ids } = node_cache[id];
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'nodeDetailTabIndex', 0);
const [
techwebRoute,
setTechwebRoute,
] = useLocalState(context, 'techwebRoute', null);
const prereqNodes = nodes.filter(x => prereq_ids.includes(x.id));
const complPrereq = prereq_ids
.filter(x => nodes.find(y => y.id === x)?.tier === 0).length;
const unlockedNodes = nodes.filter(x => unlock_ids.includes(x.id));
return (
<Flex direction="column" height="100%">
<Flex.Item shrink={1}>
<Flex justify="space-between" className="Techweb__HeaderSectionTabs">
<Flex.Item align="center" className="Techweb__HeaderTabTitle">
Node
</Flex.Item>
<Flex.Item grow={1}>
<Tabs>
<Tabs.Tab
selected={tabIndex === 0}
onClick={() => setTabIndex(0)}>
Required ({complPrereq}/{prereqNodes.length})
</Tabs.Tab>
<Tabs.Tab
selected={tabIndex === 1}
disabled={unlockedNodes.length === 0}
onClick={() => setTabIndex(1)}>
Unlocks ({unlockedNodes.length})
</Tabs.Tab>
</Tabs>
</Flex.Item>
<Flex.Item align="center">
<Button
icon="home"
onClick={() => setTechwebRoute(null)}>
Home
</Button>
</Flex.Item>
</Flex>
</Flex.Item>
<Flex.Item className="Techweb__OverviewNodes" shrink={0}>
<TechNode node={node} nodetails />
<Divider />
</Flex.Item>
{tabIndex === 0 && (
<Flex.Item className="Techweb__OverviewNodes" grow={1}>
{prereqNodes.map(n => (
<TechNode key={n.id} node={n} />
))}
</Flex.Item>
)}
{tabIndex === 1 && (
<Flex.Item className="Techweb__OverviewNodes" grow={1}>
{unlockedNodes.map(n => (
<TechNode key={n.id} node={n} />
))}
</Flex.Item>
)}
</Flex>
);
};
const TechNode = (props, context) => {
const { act, data } = useRemappedBackend(context);
const {
node_cache,
design_cache,
experiments,
points,
nodes,
} = data;
const { node, nodetails, nocontrols } = props;
const { id, can_unlock, tier } = node;
const {
name,
description,
costs,
design_ids,
prereq_ids,
required_experiments,
discount_experiments,
} = node_cache[id];
const [
techwebRoute,
setTechwebRoute,
] = useLocalState(context, 'techwebRoute', null);
const [
tabIndex,
setTabIndex,
] = useLocalState(context, 'nodeDetailTabIndex', 0);
const expcompl = required_experiments
.filter(x => experiments[x]?.completed)
.length;
const experimentProgress = (
<ProgressBar
ranges={{
good: [0.5, Infinity],
average: [0.25, 0.5],
bad: [-Infinity, 0.25],
}}
value={expcompl / required_experiments.length}>
Experiments ({expcompl}/{required_experiments.length})
</ProgressBar>
);
const techcompl = prereq_ids
.filter(x => nodes.find(y => y.id === x)?.tier === 0)
.length;
const techProgress = (
<ProgressBar
ranges={{
good: [0.5, Infinity],
average: [0.25, 0.5],
bad: [-Infinity, 0.25],
}}
value={techcompl / prereq_ids.length}>
Tech ({techcompl}/{prereq_ids.length})
</ProgressBar>
);
// Notice this logic will have te be changed if we make the discounts
// pool-specific
const nodeDiscount = Object.keys(discount_experiments)
.filter(x => experiments[x]?.completed)
.reduce((tot, curr) => {
return tot + discount_experiments[curr];
}, 0);
return (
<Section
className="Techweb__NodeContainer"
title={name}
buttons={!nocontrols && (
<>
{!nodetails && (
<Button
icon="tasks"
onClick={() => {
setTechwebRoute({ route: "details", selectedNode: id });
setTabIndex(0);
}}>
Details
</Button>
)}
{tier > 0 && (
<Button
icon="lightbulb"
disabled={!can_unlock || tier > 1}
onClick={() => act("researchNode", { node_id: id })}>
Research
</Button>
)}
</>
)}>
{tier !== 0 && (
<Flex className="Techweb__NodeProgress">
{costs.map(k => {
const reqPts = Math.max(0, k.value - nodeDiscount);
const nodeProg = Math.min(reqPts, points[k.type]) || 0;
return (
<Flex.Item key={k.type} grow={1} basis={0}>
<ProgressBar
ranges={{
good: [0.5, Infinity],
average: [0.25, 0.5],
bad: [-Infinity, 0.25],
}}
value={reqPts === 0
? 1
: Math.min(1, (points[k.type]||0) / reqPts)}>
{abbreviateName(k.type)} ({nodeProg}/{reqPts})
</ProgressBar>
</Flex.Item>
);
})}
{prereq_ids.length > 0 && (
<Flex.Item grow={1} basis={0}>
{techProgress}
</Flex.Item>
)}
{required_experiments?.length > 0 && (
<Flex.Item grow={1} basis={0}>
{experimentProgress}
</Flex.Item>
)}
</Flex>
)}
<Box className="Techweb__NodeDescription" mb={2}>
{description}
</Box>
<Box className="Techweb__NodeUnlockedDesigns" mb={2}>
{design_ids.map((k, i) => (
<Button
key={id}
className={`${design_cache[k].class} Techweb__DesignIcon`}
tooltip={design_cache[k].name}
tooltipPosition={i % 15 < 7 ? "right" : "left"} />
))}
</Box>
{required_experiments?.length > 0 && (
<Collapsible
className="Techweb__NodeExperimentsRequired"
title="Required Experiments">
{required_experiments.map(k => {
const thisExp = experiments[k];
if (thisExp === null || thisExp === undefined) {
return (
<LockedExperiment />
);
}
return (
<Experiment key={thisExp} exp={thisExp} />
);
})}
</Collapsible>
)}
{Object.keys(discount_experiments).length > 0 && (
<Collapsible
className="TechwebNodeExperimentsRequired"
title="Discount-Eligible Experiments">
{Object.keys(discount_experiments).map(k => {
const thisExp = experiments[k];
if (thisExp === null || thisExp === undefined) {
return (
<LockedExperiment />
);
}
return (
<Experiment key={thisExp} exp={thisExp}>
<Box className="Techweb__ExperimentDiscount">
Provides a discount of {discount_experiments[k]} points
to all required point pools.
</Box>
</Experiment>
);
})}
</Collapsible>
)}
</Section>
);
};
const LockedExperiment = (props, context) => {
return (
<Box m={1} className="ExperimentConfigure__ExperimentPanel">
<Button fluid
backgroundColor="#40628a"
className="ExperimentConfigure__ExperimentName"
disabled>
<Flex align="center" justify="space-between">
<Flex.Item
color="rgba(0, 0, 0, 0.6)">
<Icon name="lock" />
Undiscovered Experiment
</Flex.Item>
<Flex.Item
color="rgba(0, 0, 0, 0.5)">
???
</Flex.Item>
</Flex>
</Button>
<Box className={"ExperimentConfigure__ExperimentContent"}>
This experiment has not been discovered yet, continue researching
nodes in the tree to discover the contents of this experiment.
</Box>
</Box>
);
};
@@ -0,0 +1,98 @@
import { Component } from 'inferno';
import { useBackend } from '../backend';
import { Box, Stack } from '../components';
import { Window } from '../layouts';
export class Thermometer extends Component {
componentDidMount() {
Byond.winset(window.__windowId__, {
'transparent-color': '#242322',
});
}
componentWillUnmount() {
Byond.winset(window.__windowId__, {
'transparent-color': null,
});
}
render() {
const { act, data } = useBackend(this.context);
return (
<Window
width={70}
height={430}>
<Stack
fill
align="center"
justify="space-around"
backgroundColor="#242322"
style={{
'background-image': "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAQMAAABIeJ9nAAAABlBMVEVya3UjIyN3S/1dAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAMSURBVAjXY2hgcAAAAcQAwUlFKkkAAAAASUVORK5CYII=')",
}}>
<Stack.Item ml={1}>
<ThermometerIcon
temperature={data.Temperature}
maxTemperature={1000} />
</Stack.Item>
</Stack>
</Window>
);
}
}
const ThermometerIcon = props => {
const { temperature, maxTemperature } = props;
return (
<Box>
<Box
style={{
'position': 'relative',
'width': '22px',
'height': '340px',
'margin': '0 auto',
'background-color': '#595959',
'border': '4px solid #363636',
'border-radius': '12px',
'border-bottom': 'none',
'border-index': '0',
'box-shadow': '4px 4px #000000',
}}>
<Box
style={{
'position': 'absolute',
'width': '5x',
'bottom': 0,
'left': '0px',
'right': 0,
'transition': 'height 2s ease-out',
// Temp in %
'height': `${((temperature / maxTemperature)*100)}%`,
'background-color': '#bd2020',
'border-radius': '8px',
'border-bottom': 'none',
'z-index': '1',
}} />
</Box>
<Box
style={{
'position': 'relative',
'width': '56px',
'line-height': '48px',
'text-align': 'center',
'margin': '-8px auto 0 auto',
'background-color': '#bd2020',
'border': '4px solid #363636',
'border-spacing': '5px',
'border-radius': '35px',
'border-index': '1',
'border-bottom': '0.1',
'box-shadow': '4px 4px #000000',
'z-index': '0',
}}>
{temperature}K
</Box>
</Box>
);
};
@@ -0,0 +1,62 @@
import { useBackend } from '../backend';
import { Box, Button, Divider, Flex, Knob, LabeledControls, Section } from '../components';
import { Window } from '../layouts';
export const TrainingMachine = (props, context) => {
const { act, data } = useBackend(context);
return (
<Window
width={230}
height={150}
title="AURUMILL">
<Window.Content>
<Section fill title="Training Machine">
<LabeledControls m={1}>
<LabeledControls.Item label="Speed">
<Knob
inline
size={1.2}
step={.5}
stepPixelSize={10}
value={data.movespeed}
minValue={1}
maxValue={10}
onDrag={(e, value) => act("movespeed", { movespeed: value })}
/>
</LabeledControls.Item>
<LabeledControls.Item label="Range">
<Knob
inline
size={1.2}
step={1}
stepPixelSize={50}
value={data.range}
minValue={1}
maxValue={7}
onDrag={(e, value) => act("range", { range: value })}
/>
</LabeledControls.Item>
<Flex.Item>
<Divider vertical />
</Flex.Item>
<Flex.Item label="Simulation">
<Button
fluid
selected={data.moving}
content={(
<Box
bold
fontSize="1.4em"
lineHeight={3}>
{data.moving ? "END" : "BEGIN"}
</Box>
)}
onClick={() => act('toggle')}
/>
</Flex.Item>
</LabeledControls>
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,201 @@
import { classes } from 'common/react';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Icon, Section, Stack } from '../components';
import { Window } from '../layouts';
const DEPARTMENT2COLOR = {
Arrivals: "black",
Service: "olive",
Command: "blue",
Security: "red",
Medical: "teal",
Engineering: "yellow",
Cargo: "brown",
Departures: "white",
};
const COLOR2BLURB = {
blue: "This is the tram's current location.",
green: "This is the selected destination.",
transparent: "Click to set destination.",
};
const marginNormal = 1;
const marginDipped = 3;
const dipUnderCircle = (dest, dep) => {
const index = Object.keys(dest.dest_icons).indexOf(dep);
const dipped = index >= 1 && index <= 2;
return dipped ? marginDipped : marginNormal;
};
const BrokenTramDimmer = () => {
return (
<Dimmer>
<Stack vertical>
<Stack.Item>
<Icon
ml={7}
color="red"
name="exclamation"
size={10}
/>
</Stack.Item>
<Stack.Item fontSize="14px" color="red">
No Tram Detected!
</Stack.Item>
</Stack>
</Dimmer>
);
};
const MovingTramDimmer = () => {
return (
<Dimmer>
<Stack vertical>
<Stack.Item>
<Icon
ml={10}
name="sync-alt"
color="green"
size={11}
/>
</Stack.Item>
<Stack.Item mt={5} fontSize="14px" color="green">
The tram is travelling to {current_loc[0].name}!
</Stack.Item>
</Stack>
</Dimmer>
);
};
export const TramControl = (props, context) => {
const { act, data } = useBackend(context);
const {
broken,
moving,
destinations,
} = data;
const current_loc = (destinations ? destinations.filter(
dest => dest.here === 1) : null);
const [
transitIndex,
setTransitIndex,
] = useLocalState(context, 'transit-index', 1);
const MovingTramDimmer = () => {
return (
<Dimmer>
<Stack vertical>
<Stack.Item>
<Icon
ml={10}
name="sync-alt"
color="green"
size={11}
/>
</Stack.Item>
<Stack.Item mt={5} fontSize="14px" color="green">
The tram is travelling to {current_loc[0].name}!
</Stack.Item>
</Stack>
</Dimmer>
);
};
const Destination = props => {
const { dest } = props;
const getDestColor = dest => {
const here = dest.name === current_loc[0].name;
const selected = transitIndex === destinations.indexOf(dest);
return !current_loc ? "bad" : here ? "blue" : selected ? "green" : "transparent";
};
return (
<Stack vertical>
<Stack.Item ml={5}>
<Button
mr={4.38}
color={getDestColor(dest)}
circular
compact
height={5}
width={5}
tooltipPosition="top"
tooltip={COLOR2BLURB[getDestColor(dest)]}
onClick={() => setTransitIndex(destinations.indexOf(dest))} >
<Icon ml={-2.1} mt={0.55} fontSize="60px" name="circle-o" />
</Button>
{destinations.length-1 !== destinations.indexOf(dest) && (
<Section title=" " mt={-7.3} ml={10} mr={-6.1} />
) || (
<Box mt={-2.3} />
)}
</Stack.Item>
{dest.dest_icons && (
<Stack.Item >
<Stack>
{Object.keys(dest.dest_icons).map(dep => (
<Stack.Item key={dep}
mt={dipUnderCircle(dest, dep)}>
<Button
color={DEPARTMENT2COLOR[dep]}
icon={dest.dest_icons[dep]}
tooltipPosition="bottom"
tooltip={dep}
style={{
'border-radius': '5em',
'border': '2px solid white',
}}
/>
</Stack.Item>
))}
</Stack>
</Stack.Item>
)}
</Stack>
);
};
return (
<Window
title="Tram Controls"
width={600}
height={300}>
<Window.Content>
{!!broken && (
<BrokenTramDimmer />
) || (
<Section fill>
{!!moving && (
<MovingTramDimmer />
)}
<Stack ml="-6px" vertical fill>
<Stack.Item grow fontSize="16px" mt={1} mb={9} textAlign="center">
Nanotrasen Transit System
</Stack.Item>
<Stack.Item mb={4}>
<Stack fill>
<Stack.Item grow={2} />
{destinations.map(dest => (
<Stack.Item key={dest.name} grow={1} >
<Destination dest={dest} />
</Stack.Item>
))}
<Stack.Item grow={1} />
</Stack>
</Stack.Item>
<Stack.Item fontSize="16px" mt={1} mb={9} textAlign="center" grow>
<Button
disabled={
current_loc[0].name === destinations[transitIndex].name
}
content="Send Tram"
onClick={() => act('send', {
destination: destinations[transitIndex].name,
})} />
</Stack.Item>
</Stack>
</Section>
)}
</Window.Content>
</Window>
);
};
+194
View File
@@ -0,0 +1,194 @@
import { useBackend } from '../backend';
import { Box, Icon, Stack, Button, Section, NoticeBox, LabeledList, Collapsible } from '../components';
import { Window } from '../layouts';
export const Vote = (props, context) => {
const { data } = useBackend(context);
const { mode, question, lower_admin } = data;
/**
* Adds the voting type to title if there is an ongoing vote.
*/
let windowTitle = 'Vote';
if (mode) {
windowTitle += ': ' + (question || mode).replace(/^\w/, (c) => c.toUpperCase());
}
return (
<Window resizable title={windowTitle} width={400} height={500}>
<Window.Content>
<Stack fill vertical>
<Section title="Create Vote">
<VoteOptions />
{!!lower_admin && <VotersList />}
</Section>
<ChoicesPanel />
<TimePanel />
</Stack>
</Window.Content>
</Window>
);
};
/**
* The create vote options menu. Only upper admins can disable voting.
* @returns A section visible to everyone with vote options.
*/
const VoteOptions = (props, context) => {
const { act, data } = useBackend(context);
const {
allow_vote_restart,
allow_vote_map,
lower_admin,
upper_admin,
} = data;
return (
<Stack.Item>
<Collapsible title="Start a Vote">
<Stack justify="space-between">
<Stack.Item>
<Stack vertical>
<Stack.Item>
{!!lower_admin && (
<Button.Checkbox
mr={!allow_vote_map ? 1 : 1.6}
color="red"
checked={!!allow_vote_map}
disabled={!upper_admin}
onClick={() => act('toggle_map')}>
{allow_vote_map ? 'Enabled' : 'Disabled'}
</Button.Checkbox>
)}
<Button disabled={!allow_vote_map} onClick={() => act('map')}>
Map
</Button>
</Stack.Item>
<Stack.Item>
{!!lower_admin && (
<Button.Checkbox
mr={!allow_vote_restart ? 1 : 1.6}
color="red"
checked={!!allow_vote_restart}
disabled={!upper_admin}
onClick={() => act('toggle_restart')}>
{allow_vote_restart ? 'Enabled' : 'Disabled'}
</Button.Checkbox>
)}
<Button
disabled={!allow_vote_restart}
onClick={() => act('restart')}>
Restart
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
{!!lower_admin && (
<Button disabled={!lower_admin} onClick={() => act('custom')}>
Create Custom Vote
</Button>
)}
</Stack.Item>
</Stack>
</Collapsible>
</Stack.Item>
);
};
/**
* View Voters by ckey. Admin only.
* @returns A collapsible list of voters
*/
const VotersList = (props, context) => {
const { data } = useBackend(context);
const { voting } = data;
return (
<Stack.Item>
<Collapsible title={`View Voters${voting.length ? `: ${voting.length}` : ""}`}>
<Section height={8} fill scrollable>
{voting.map((voter) => {
return <Box key={voter}>{voter}</Box>;
})}
</Section>
</Collapsible>
</Stack.Item>
);
};
/**
* The choices panel which displays all options in the list.
* @returns A section visible to all users.
*/
const ChoicesPanel = (props, context) => {
const { act, data } = useBackend(context);
const { choices, selected_choice } = data;
return (
<Stack.Item grow>
<Section fill scrollable title="Choices">
{choices.length !== 0 ? (
<LabeledList>
{choices.map((choice, i) => (
<Box key={choice.id}>
<LabeledList.Item
label={choice.name.replace(/^\w/, (c) => c.toUpperCase())}
textAlign="right"
buttons={
<Button
disabled={i === selected_choice - 1}
onClick={() => {
act('vote', { index: i + 1 });
}}>
Vote
</Button>
}>
{i === selected_choice - 1 && (
<Icon
alignSelf="right"
mr={2}
color="green"
name="vote-yea"
/>
)}
{choice.votes} Votes
</LabeledList.Item>
<LabeledList.Divider />
</Box>
))}
</LabeledList>
) : (
<NoticeBox>No choices available!</NoticeBox>
)}
</Section>
</Stack.Item>
);
};
/**
* Countdown timer at the bottom. Includes a cancel vote option for admins.
* @returns A section visible to everyone.
*/
const TimePanel = (props, context) => {
const { act, data } = useBackend(context);
const { lower_admin, time_remaining } = data;
return (
<Stack.Item mt={1}>
<Section>
<Stack justify="space-between">
<Box fontSize={1.5}>Time Remaining: {time_remaining || 0}s</Box>
{!!lower_admin && (
<Button
color="red"
disabled={!lower_admin}
onClick={() => act('cancel')}>
Cancel Vote
</Button>
)}
</Stack>
</Section>
</Stack.Item>
);
};
@@ -0,0 +1,127 @@
import { useBackend } from '../../backend';
import { Box, Button, Icon, LabeledList } from '../../components';
export const ReagentLookup = (props, context) => {
const { reagent } = props;
const { act } = useBackend(context);
if (!reagent) {
return (
<Box>
No reagent selected!
</Box>
);
}
return (
<LabeledList>
<LabeledList.Item label="Reagent">
<Icon name="circle" mr={1} color={reagent.reagentCol} />
{reagent.name}
<Button
ml={1}
icon="wifi"
color="teal"
tooltip="Open the associated wikipage for this reagent."
tooltipPosition="left"
onClick={() => {
Byond.command(`wiki Guide_to_chemistry#${reagent.name}`);
}} />
</LabeledList.Item>
<LabeledList.Item label="Description">
{reagent.desc}
</LabeledList.Item>
<LabeledList.Item label="pH">
<Icon name="circle" mr={1} color={reagent.pHCol} />
{reagent.pH}
</LabeledList.Item>
<LabeledList.Item label="Properties">
<LabeledList>
{!!reagent.OD && (
<LabeledList.Item label="Overdose">
{reagent.OD}u
</LabeledList.Item>
)}
{reagent.addictions[0] && (
<LabeledList.Item label="Addiction">
{reagent.addictions.map(addiction => (
<Box key={addiction}>
{addiction}
</Box>
))}
</LabeledList.Item>
)}
<LabeledList.Item label="Metabolization rate">
{reagent.metaRate}u/s
</LabeledList.Item>
</LabeledList>
</LabeledList.Item>
<LabeledList.Item label="Impurities">
<LabeledList>
{reagent.impureReagent && (
<LabeledList.Item label="Impure reagent">
<Button
icon="vial"
tooltip="This reagent will partially convert into this when the purity is above the Inverse purity on consumption."
tooltipPosition="left"
content={reagent.impureReagent}
onClick={() => act('reagent_click', {
id: reagent.impureId,
})} />
</LabeledList.Item>
)}
{reagent.inverseReagent && (
<LabeledList.Item label="Inverse reagent">
<Button
icon="vial"
content={reagent.inverseReagent}
tooltip="This reagent will convert into this when the purity is below the Inverse purity on consumption."
tooltipPosition="left"
onClick={() => act('reagent_click', {
id: reagent.inverseId,
})} />
</LabeledList.Item>
)}
{reagent.failedReagent && (
<LabeledList.Item label="Failed reagent">
<Button
icon="vial"
tooltip="This reagent will turn into this if the purity of the reaction is below the minimum purity on completion."
tooltipPosition="left"
content={reagent.failedReagent}
onClick={() => act('reagent_click', {
id: reagent.failedId,
})} />
</LabeledList.Item>
)}
</LabeledList>
{reagent.isImpure && (
<Box>
This reagent is created by impurity.
</Box>
)}
{reagent.deadProcess && (
<Box>
This reagent works on the dead.
</Box>
)}
{!reagent.failedReagent
&& !reagent.inverseReagent
&& !reagent.impureReagent && (
<Box>
This reagent has no impure reagents.
</Box>
)}
</LabeledList.Item>
<LabeledList.Item>
<Button
icon="flask"
mt={2}
content={"Find associated reaction"}
color="purple"
onClick={() => act('find_reagent_reaction', {
id: reagent.id,
})} />
</LabeledList.Item>
</LabeledList>
);
};
@@ -0,0 +1,229 @@
import { useBackend } from '../../backend';
import { Box, Button, Chart, Flex, Icon, LabeledList, Tooltip } from '../../components';
export const RecipeLookup = (props, context) => {
const { recipe, bookmarkedReactions } = props;
const { act, data } = useBackend(context);
if (!recipe) {
return (
<Box>
No reaction selected!
</Box>
);
}
const getReaction = id => {
return data.master_reaction_list.filter(reaction => (
reaction.id === id
));
};
const addBookmark = bookmark => {
bookmarkedReactions.add(bookmark);
};
return (
<LabeledList>
<LabeledList.Item bold label="Recipe">
<Icon name="circle" mr={1} color={recipe.reagentCol} />
{recipe.name}
<Button
icon="arrow-left"
ml={3}
disabled={recipe.subReactIndex === 1}
onClick={() => act('reduce_index', {
id: recipe.name,
})} />
<Button
icon="arrow-right"
disabled={recipe.subReactIndex === recipe.subReactLen}
onClick={() => act('increment_index', {
id: recipe.name,
})} />
{bookmarkedReactions && (
<Button
icon="book"
color="green"
disabled={bookmarkedReactions.has(getReaction(recipe.id)[0])}
onClick={() => {
addBookmark(getReaction(recipe.id)[0]);
act('update_ui');
}} />
)}
</LabeledList.Item>
{recipe.products && (
<LabeledList.Item bold label="Products">
{recipe.products.map(product => (
<Button
key={product.name}
icon="vial"
disabled={product.hasProduct}
content={product.ratio + "u " + product.name}
onClick={() => act('reagent_click', {
id: product.id,
})} />
))}
</LabeledList.Item>
)}
<LabeledList.Item bold label="Reactants">
{recipe.reactants.map(reactant => (
<Box key={reactant.id}>
<Button
icon="vial"
color={reactant.color}
content={reactant.ratio + "u " + reactant.name}
onClick={() => act('reagent_click', {
id: reactant.id,
})} />
{!!reactant.tooltipBool && (
<Button
icon="flask"
color="purple"
tooltip={reactant.tooltip}
tooltipPosition="right"
onClick={() => act('find_reagent_reaction', {
id: reactant.id,
})} />
)}
</Box>
))}
</LabeledList.Item>
{recipe.catalysts && (
<LabeledList.Item bold label="Catalysts">
{recipe.catalysts.map(catalyst => (
<Box key={catalyst.id}>
{catalyst.tooltipBool && (
<Button
icon="vial"
color={catalyst.color}
content={catalyst.ratio + "u " + catalyst.name}
tooltip={catalyst.tooltip}
tooltipPosition={"right"}
onClick={() => act('reagent_click', {
id: catalyst.id,
})} />
) || (
<Button
icon="vial"
color={catalyst.color}
content={catalyst.ratio + "u " + catalyst.name}
onClick={() => act('reagent_click', {
id: catalyst.id,
})} />
)}
</Box>
))}
</LabeledList.Item>
)}
{recipe.reqContainer && (
<LabeledList.Item bold label="Container">
<Button
color="transparent"
textColor="white"
tooltipPosition="right"
content={recipe.reqContainer}
tooltip="The required container for this reaction to occur in." />
</LabeledList.Item>
)}
<LabeledList.Item bold label="Purity">
<LabeledList>
<LabeledList.Item label="Optimal pH range">
<Box position="relative">
<Tooltip
content="If your reaction is kept within these bounds then the purity of your product will be 100%" />
{recipe.lowerpH + "-" + recipe.upperpH}
</Box>
</LabeledList.Item>
{!!recipe.inversePurity && (
<LabeledList.Item label="Inverse purity">
<Box position="relative">
<Tooltip
content="If your purity is below this it will 100% convert into the product's associated Inverse reagent on consumption." />
{`<${(recipe.inversePurity*100)}%`}
</Box>
</LabeledList.Item>
)}
{!!recipe.minPurity && (
<LabeledList.Item label="Minimum purity">
<Box position="relative">
<Tooltip
content="If your purity is below this at any point during the reaction, it will cause negative effects, and if it remains below this value on completion it will convert into the product's associated Failed reagent." />
{`<${(recipe.minPurity*100)}%`}
</Box>
</LabeledList.Item>
)}
</LabeledList>
</LabeledList.Item>
<LabeledList.Item bold label="Rate profile" width="10px">
<Box
height="50px"
position="relative"
style={{
'background-color': 'black',
}}>
<Chart.Line
fillPositionedParent
data={recipe.thermodynamics}
strokeWidth={0}
fillColor={"#3cf072"} />
{recipe.explosive && (
<Chart.Line
position="absolute"
justify="right"
top={0.01}
bottom={0}
right={recipe.isColdRecipe ? null : 0}
width="28px"
data={recipe.explosive}
strokeWidth={0}
fillColor={"#d92727"} />
)}
</Box>
<Flex
justify="space-between">
<Flex.Item
position="relative"
textColor={recipe.isColdRecipe && "red"}>
<Tooltip
content={recipe.isColdRecipe
? "The temperature at which it is underheated, causing negative effects on the reaction."
: "The minimum temperature needed for this reaction to start. Heating it up past this point will increase the reaction rate."} />
{recipe.isColdRecipe
? recipe.explodeTemp + "K"
: recipe.tempMin + "K"}
</Flex.Item>
{recipe.explosive && (
<Flex.Item
position="relative"
textColor={!recipe.isColdRecipe && "red"}>
<Tooltip
content={recipe.isColdRecipe
? "The minimum temperature needed for this reaction to start. Heating it up past this point will increase the reaction rate."
: "The temperature at which it is overheated, causing negative effects on the reaction."} />
{recipe.isColdRecipe
? recipe.tempMin + "K"
: recipe.explodeTemp + "K"}
</Flex.Item>
)}
</Flex>
</LabeledList.Item>
<LabeledList.Item bold label="Dynamics">
<LabeledList>
<LabeledList.Item label="Optimal rate">
<Box position="relative">
<Tooltip
content="The fastest rate the reaction can go, in units per second. This is the plateu region shown in the rate profile above." />
{recipe.thermoUpper + "u/s"}
</Box>
</LabeledList.Item>
</LabeledList>
<Box
position="relative">
<Tooltip
content="The heat generated by a reaction - exothermic produces heat, endothermic consumes heat." />
{recipe.thermics}
</Box>
</LabeledList.Item>
</LabeledList>
);
};
+5 -5
View File
@@ -13,17 +13,17 @@ export const captureExternalLinks = () => {
/** @type {HTMLElement} */
let target = e.target;
// Recurse down the tree to find a valid link
while (target && target !== document.body) {
while (true) {
// Reached the end, bail.
if (!target || target === document.body) {
return;
}
const tagName = String(target.tagName).toLowerCase();
if (tagName === 'a') {
break;
}
target = target.parentElement;
}
// Not a link, do nothing.
if (!target) {
return;
}
const hrefAttr = target.getAttribute('href') || '';
// Leave BYOND links alone
const isByondLink = hrefAttr.charAt(0) === '?'
+2 -2
View File
@@ -4,10 +4,10 @@
"version": "4.3.0",
"dependencies": {
"common": "workspace:*",
"dompurify": "^2.2.6",
"dompurify": "^2.2.7",
"inferno": "^7.4.8",
"inferno-vnode-flags": "^7.4.8",
"marked": "^2.0.0",
"marked": "^2.0.3",
"tgui-dev-server": "workspace:*",
"tgui-polyfill": "workspace:*"
}
+20 -8
View File
@@ -8,7 +8,7 @@ import { selectBackend } from './backend';
import { selectDebug } from './debug/selectors';
import { Window } from './layouts';
const requireInterface = require.context('./interfaces', false, /\.js$/);
const requireInterface = require.context('./interfaces');
const routingError = (type, name) => () => {
return (
@@ -47,15 +47,27 @@ export const getRoutedComponent = store => {
}
}
const name = config?.interface;
const interfacePathBuilders = [
name => `./${name}.tsx`,
name => `./${name}.js`,
name => `./${name}/index.tsx`,
name => `./${name}/index.js`,
];
let esModule;
try {
esModule = requireInterface(`./${name}.js`);
}
catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
return routingError('notFound', name);
while (!esModule && interfacePathBuilders.length > 0) {
const interfacePathBuilder = interfacePathBuilders.shift();
const interfacePath = interfacePathBuilder(name);
try {
esModule = requireInterface(interfacePath);
}
throw err;
catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err;
}
}
}
if (!esModule) {
return routingError('notFound', name);
}
const Component = esModule[name];
if (!Component) {
@@ -16,6 +16,7 @@ $border-radius: base.$border-radius !default;
left: 0;
right: 0;
bottom: 0;
font-family: Verdana, sans-serif;
font-style: normal;
font-weight: normal;
@@ -0,0 +1,58 @@
@use '../colors.scss';
$department_map: (
'Command': colors.$yellow,
'Security': colors.$red,
'Engineering': colors.$orange,
'Medical': colors.$teal,
'Misc': colors.$white,
'Science': colors.$purple,
'Supply': colors.$brown,
'Service': colors.$green,
'Silicon': colors.$pink
);
.CrewManifest {
@each $department-name, $color-value in $department_map {
&--#{$department-name} {
.Section {
&__title {
border-color: $color-value;
}
&__titleText {
color: $color-value;
}
}
}
}
&__Cell {
padding: 3px 0;
&--Rank {
color: colors.$label;
}
}
&__Icons {
padding: 3px 9px;
text-align: right;
}
&__Icon {
color: colors.$label;
position: relative;
&:not(:last-child) {
margin-right: 7px;
}
&--Chevron {
padding-right: 2px;
}
&--Command {
color: colors.$yellow;
}
}
}

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