okay
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint",
|
||||
"version": "7.19.0-pnpify",
|
||||
"version": "7.21.0-pnpify",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
@@ -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`));
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "4.1.5-pnpify",
|
||||
"version": "4.2.3-pnpify",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/dist
|
||||
@@ -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 |
@@ -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 |
@@ -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) {}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
);
|
||||
};
|
||||
@@ -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'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 'em.
|
||||
<br /><br />
|
||||
If you run out of food or crew,
|
||||
it'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'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'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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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'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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 → Disk
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
icon="upload"
|
||||
onClick={() => act("uploadDisk", { type: diskType })}>
|
||||
Disk → 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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) === '?'
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||