Merge pull request #14658 from silicons/backend_sync
Updates tgui and build backend, fixes the build.bat for those with latest eslint (?)
@@ -4,7 +4,7 @@ indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
# end_of_line = lf
|
||||
end_of_line = lf
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# dmdoc
|
||||
[DOCUMENTATION]: http://codedocs.tgstation13.org
|
||||
|
||||
[BYOND]: https://secure.byond.com/
|
||||
|
||||
[DMDOC]: https://github.com/SpaceManiac/SpacemanDMM/tree/master/src/dmdoc
|
||||
|
||||
[DMDOC] is a documentation generator for DreamMaker, the scripting language
|
||||
of the [BYOND] game engine. It produces simple static HTML files based on
|
||||
documented files, macros, types, procs, and vars.
|
||||
|
||||
We use **dmdoc** to generate [DOCUMENTATION] for our code, and that documentation
|
||||
is automatically generated and built on every new commit to the master branch
|
||||
|
||||
This gives new developers a clickable reference [DOCUMENTATION] they can browse to better help
|
||||
gain understanding of the /tg/station codebase structure and api reference.
|
||||
|
||||
## Documenting code on /tg/station
|
||||
We use block comments to document procs and classes, and we use `///` line comments
|
||||
when documenting individual variables.
|
||||
|
||||
It is required that all new code be covered with DMdoc code, according to the [Requirements](#Required)
|
||||
|
||||
We also require that when you touch older code, you must document the functions that you
|
||||
have touched in the process of updating that code
|
||||
|
||||
### Required
|
||||
A class *must* always be autodocumented, and all public functions *must* be documented
|
||||
|
||||
All class level defined variables *must* be documented
|
||||
|
||||
Internal functions *should* be documented, but may not be
|
||||
|
||||
A public function is any function that a developer might reasonably call while using
|
||||
or interacting with your object. Internal functions are helper functions that your
|
||||
public functions rely on to implement logic
|
||||
|
||||
|
||||
### Documenting a proc
|
||||
When documenting a proc, we give a short one line description (as this is shown
|
||||
next to the proc definition in the list of all procs for a type or global
|
||||
namespace), then a longer paragraph which will be shown when the user clicks on
|
||||
the proc to jump to it's definition
|
||||
```
|
||||
/**
|
||||
* Short description of the proc
|
||||
*
|
||||
* Longer detailed paragraph about the proc
|
||||
* including any relevant detail
|
||||
* Arguments:
|
||||
* * arg1 - Relevance of this argument
|
||||
* * arg2 - Relevance of this argument
|
||||
*/
|
||||
```
|
||||
|
||||
### Documenting a class
|
||||
We first give the name of the class as a header, this can be omitted if the name is
|
||||
just going to be the typepath of the class, as dmdoc uses that by default
|
||||
|
||||
Then we give a short oneline description of the class
|
||||
|
||||
Finally we give a longer multi paragraph description of the class and it's details
|
||||
```
|
||||
/**
|
||||
* # Classname (Can be omitted if it's just going to be the typepath)
|
||||
*
|
||||
* The short overview
|
||||
*
|
||||
* A longer
|
||||
* paragraph of functionality about the class
|
||||
* including any assumptions/special cases
|
||||
*
|
||||
*/
|
||||
```
|
||||
|
||||
### Documenting a variable/define
|
||||
Give a short explanation of what the variable, in the context of the class, or define is.
|
||||
```
|
||||
/// Type path of item to go in suit slot
|
||||
var/suit = null
|
||||
```
|
||||
|
||||
## Module level description of code
|
||||
Modules are the best way to describe the structure/intent of a package of code
|
||||
where you don't want to be tied to the formal layout of the class structure.
|
||||
|
||||
On /tg/station we do this by adding markdown files inside the `code` directory
|
||||
that will also be rendered and added to the modules tree. The structure for
|
||||
these is deliberately not defined, so you can be as freeform and as wheeling as
|
||||
you would like.
|
||||
|
||||
[Here is a representative example of what you might write](http://codedocs.tgstation13.org/code/modules/keybindings/readme.html)
|
||||
|
||||
## Special variables
|
||||
You can use certain special template variables in DM DOC comments and they will be expanded
|
||||
```
|
||||
[DEFINE_NAME] - Expands to a link to the define definition if documented
|
||||
[/mob] - Expands to a link to the docs for the /mob class
|
||||
[/mob/proc/Dizzy] - Expands to a link that will take you to the /mob class and anchor you to the dizzy proc docs
|
||||
[/mob/var/stat] - Expands to a link that will take you to the /mob class and anchor you to the stat var docs
|
||||
```
|
||||
|
||||
You can customise the link name by using `[link name][link shorthand].`
|
||||
|
||||
eg. `[see more about dizzy here] [/mob/proc/Dizzy]`
|
||||
|
||||
This is very useful to quickly link to other parts of the autodoc code to expand
|
||||
upon a comment made, or reasoning about code
|
||||
@@ -1,8 +1,29 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
## Reporting Issues
|
||||
1. [Reporting Issues](#reporting-issues)
|
||||
2. [Introduction](#introduction)
|
||||
3. [Getting Started](#getting-started)
|
||||
4. [Meet the Team](#meet-the-team)
|
||||
1. [Headcoder](#headcoder)
|
||||
2. [Maintainers](#maintainers)
|
||||
3. [Issue Managers](#issue-managers)
|
||||
5. [Specifications](#specifications)
|
||||
6. [Pull Request Process](#pull-request-process)
|
||||
7. [Porting features/sprites/sounds/tools from other codebases](#porting-featuresspritessoundstools-from-other-codebases)
|
||||
8. [Banned content](#banned-content)
|
||||
9. [A word on Git](#a-word-on-git)
|
||||
|
||||
See [this page](http://tgstation13.org/wiki/Reporting_Issues) for a guide and format to issue reports.
|
||||
## Reporting Issues
|
||||
If you ever encounter a bug in-game, the best way to let a coder know about it is with our GitHub Issue Tracker. Please make sure you use the supplied issue template, and include the round ID for the server.
|
||||
|
||||
(If you don't have an account, making a new one takes only one minute.)
|
||||
|
||||
If you have difficulty, ask for help in the #coding-general channel on our discord.
|
||||
|
||||
## Citadel Station 13
|
||||
Notice: We are currently using a near-copy of /tg/station's CONTRIBUTING.md file. Keep in mind which codebase you are on.
|
||||
Any questions regarding help for development/spriting/coding/etc should go on #development-discussion-main on our discord.
|
||||
https://discord.gg/fkVVfVH48n
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -15,35 +36,63 @@ First things first, we want to make it clear how you can contribute (if you've n
|
||||
/tg/station doesn't have a list of goals and features to add; we instead allow freedom for contributors to suggest and create their ideas for the game. That doesn't mean we aren't determined to squash bugs, which unfortunately pop up a lot due to the deep complexity of the game. Here are some useful starting guides, if you want to contribute or if you want to know what challenges you can tackle with zero knowledge about the game's code structure.
|
||||
|
||||
If you want to contribute the first thing you'll need to do is [set up Git](http://tgstation13.org/wiki/Setting_up_git) so you can download the source code.
|
||||
After setting it up, optionally navigate your git commandline to the project folder and run the command: `git config blame.ignoreRevsFile .git-blame-ignore-revs`.
|
||||
|
||||
We have a [list of guides on the wiki](http://www.tgstation13.org/wiki/index.php/Guides#Development_and_Contribution_Guides) that will help you get started contributing to /tg/station with Git and Dream Maker. For beginners, it is recommended you work on small projects like bugfixes at first. If you need help learning to program in BYOND, check out this [repository of resources](http://www.byond.com/developer/articles/resources).
|
||||
We have a [list of guides on the wiki](http://www.tgstation13.org/wiki/Guides#Development_and_Contribution_Guides) that will help you get started contributing to /tg/station with Git and Dream Maker. For beginners, it is recommended you work on small projects like bugfixes at first. If you need help learning to program in BYOND, check out this [repository of resources](http://www.byond.com/developer/articles/resources).
|
||||
|
||||
There is an open list of approachable issues for [your inspiration here](https://github.com/tgstation/-tg-station/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22).
|
||||
There is an open list of approachable issues for [your inspiration here](https://github.com/Citadel-Station-13/Citadel-Station-13/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22).
|
||||
|
||||
You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/coderbus) on irc.rizon.net. We're just here to have fun and help out, so please don't expect professional support.
|
||||
You can of course, as always, ask for help on the Discord channels or the forums. We're just here to have fun and help out, so please don't expect professional support.
|
||||
|
||||
## Meet the Team
|
||||
|
||||
**Design Lead**
|
||||
|
||||
The Design Lead has the final say on what gameplay changes get into and out of the game. He or she has full veto power on any feature or balance additions, changes, or removals, and establishes a general, personally-preferred direction for the game.
|
||||
|
||||
**Headcoder**
|
||||
### Headcoder
|
||||
|
||||
The Headcoder is responsible for controlling, adding, and removing maintainers from the project. In addition to filling the role of a normal maintainer, they have sole authority on who becomes a maintainer, as well as who remains a maintainer and who does not.
|
||||
|
||||
**Art Director**
|
||||
|
||||
The Art Director controls sprites and aesthetic that get into the game. While sprites for brand-new additions are generally accepted regardless of quality, modified current art assets fall to the Art Director, who can decide whether or not a sprite tweak is both merited and a suitable replacement.
|
||||
|
||||
They also control the general "perspective" of the game - how sprites should generally look, especially the angle from which they're viewed. An example of this is the [3/4 perspective](http://static.tvtropes.org/pmwiki/pub/images/kakarikovillage.gif), which is a bird's eye view from above the object being viewed.
|
||||
|
||||
**Maintainers**
|
||||
### Maintainers
|
||||
|
||||
Maintainers are quality control. If a proposed pull request doesn't meet the following specifications, they can request you to change it, or simply just close the pull request. Maintainers are required to give a reason for closing the pull request.
|
||||
|
||||
Maintainers can revert your changes if they feel they are not worth maintaining or if they did not live up to the quality specifications.
|
||||
|
||||
<details>
|
||||
<summary>Maintainer Guidelines</summary>
|
||||
|
||||
These are the few directives we have for project maintainers.
|
||||
|
||||
- Do not merge PRs you create.
|
||||
- Do not merge PRs until 24 hours have passed since it was opened. Exceptions include:
|
||||
- Emergency fixes.
|
||||
- Try to get secondary maintainer approval before merging if you are able to.
|
||||
- PRs with empty commits intended to generate a changelog.
|
||||
- Do not merge PRs that contain content from the [banned content list](./CONTRIBUTING.md#banned-content).
|
||||
|
||||
These are not steadfast rules as maintainers are expected to use their best judgement when operating.
|
||||
|
||||
Our team is entirely voluntary, as such we extend our thanks to maintainers, issue managers, and contributors alike for helping keep the project alive.
|
||||
|
||||
</details>
|
||||
|
||||
### Issue Managers
|
||||
|
||||
Issue Managers help out the project by labelling bug reports and PRs and closing bug reports which are duplicates or are no longer applicable.
|
||||
|
||||
<details>
|
||||
<summary>What You Can and Can't Do as an Issue Manager</summary>
|
||||
|
||||
This should help you understand what you can and can't do with your newfound github permissions.
|
||||
|
||||
Things you **CAN** do:
|
||||
* Label issues appropriately
|
||||
* Close issues when appropriate
|
||||
* Label PRs when appropriate
|
||||
|
||||
Things you **CAN'T** do:
|
||||
* [Close PRs](https://imgur.com/w2RqpX8.png): Only maintainers are allowed to close PRs. Do not hit that button.
|
||||
|
||||
</details>
|
||||
|
||||
## Specifications
|
||||
|
||||
As mentioned before, you are expected to follow these specifications in order to make everyone's lives easier. It'll save both your time and ours, by making sure you don't have to make any changes and we don't have to ask you to. Thank you for reading this section!
|
||||
@@ -57,6 +106,7 @@ As BYOND's Dream Maker (henceforth "DM") is an object-oriented language, code mu
|
||||
DM will allow you nest almost any type keyword into a block, such as:
|
||||
|
||||
```DM
|
||||
// Not our style!
|
||||
datum
|
||||
datum1
|
||||
var
|
||||
@@ -77,7 +127,7 @@ datum
|
||||
proc3()
|
||||
code
|
||||
proc2()
|
||||
..()
|
||||
. = ..()
|
||||
code
|
||||
```
|
||||
|
||||
@@ -86,6 +136,7 @@ The use of this is not allowed in this project as it makes finding definitions v
|
||||
The previous code made compliant:
|
||||
|
||||
```DM
|
||||
// Matches /tg/station style.
|
||||
/datum/datum1
|
||||
var/varname1
|
||||
var/varname2
|
||||
@@ -101,18 +152,50 @@ The previous code made compliant:
|
||||
/datum/datum1/datum2/proc/proc3()
|
||||
code
|
||||
/datum/datum1/datum2/proc2()
|
||||
..()
|
||||
. = ..()
|
||||
code
|
||||
```
|
||||
|
||||
### All `process` procs need to make use of delta-time and be frame independent
|
||||
|
||||
In a lot of our older code, `process()` is frame dependent. Here's some example mob code:
|
||||
|
||||
```DM
|
||||
/mob/testmob
|
||||
var/health = 100
|
||||
var/health_loss = 4 //We want to lose 2 health per second, so 4 per SSmobs process
|
||||
|
||||
/mob/testmob/process(delta_time) //SSmobs runs once every 2 seconds
|
||||
health -= health_loss
|
||||
```
|
||||
|
||||
As the mobs subsystem runs once every 2 seconds, the mob now loses 4 health every process, or 2 health per second. This is called frame dependent programming.
|
||||
|
||||
Why is this an issue? If someone decides to make it so the mobs subsystem processes once every second (2 times as fast), your effects in process() will also be two times as fast. Resulting in 4 health loss per second rather than 2.
|
||||
|
||||
How do we solve this? By using delta-time. Delta-time is the amount of seconds you would theoretically have between 2 process() calls. In the case of the mobs subsystem, this would be 2 (As there is 2 seconds between every call in `process()`). Here is a new example using delta-time:
|
||||
|
||||
```DM
|
||||
/mob/testmob
|
||||
var/health = 100
|
||||
var/health_loss = 2 //Health loss every second
|
||||
|
||||
/mob/testmob/process(delta_time) //SSmobs runs once every 2 seconds
|
||||
health -= health_loss * delta_time
|
||||
```
|
||||
|
||||
In the above example, we made our health_loss variable a per second value rather than per process. In the actual process() proc we then make use of deltatime. Because SSmobs runs once every 2 seconds. Delta_time would have a value of 2. This means that by doing health_loss * delta_time, you end up with the correct amount of health_loss per process, but if for some reason the SSmobs subsystem gets changed to be faster or slower in a PR, your health_loss variable will work the same.
|
||||
|
||||
For example, if SSmobs is set to run once every 4 seconds, it would call process once every 4 seconds and multiply your health_loss var by 4 before subtracting it. Ensuring that your code is frame independent.
|
||||
|
||||
### No overriding type safety checks
|
||||
The use of the : operator to override type safety checks is not allowed. You must cast the variable to the proper type.
|
||||
|
||||
### Type paths must begin with a /
|
||||
### Type paths must begin with a `/`
|
||||
eg: `/datum/thing`, not `datum/thing`
|
||||
|
||||
### Type paths must be lowercase
|
||||
eg: `/datum/thing/blue`, not `datum/thing/BLUE` or `datum/thing/Blue`
|
||||
### Type paths must be snake case
|
||||
eg: `/datum/blue_bird`, not `/datum/BLUEBIRD` or `/datum/BlueBird` or `/datum/Bluebird` or `/datum/blueBird`
|
||||
|
||||
### Datum type paths must began with "datum"
|
||||
In DM, this is optional, but omitting it makes finding definitions harder.
|
||||
@@ -128,16 +211,28 @@ var/path_type = /obj/item/baseball_bat
|
||||
var/path_type = "/obj/item/baseball_bat"
|
||||
```
|
||||
|
||||
### Use var/name format when declaring variables
|
||||
### Use `var/name` format when declaring variables
|
||||
While DM allows other ways of declaring variables, this one should be used for consistency.
|
||||
|
||||
### Tabs, not spaces
|
||||
You must use tabs to indent your code, NOT SPACES.
|
||||
|
||||
(You may use spaces to align something, but you should tab to the block level first, then add the remaining spaces)
|
||||
Do not use tabs/spaces for indentation in the middle of a code line. Not only is this inconsistent because the size of a tab is undefined, but it means that, should the line you're aligning to change size at all, we have to adjust a ton of other code. Plus, it often time hurts readability.
|
||||
|
||||
```dm
|
||||
// Bad
|
||||
#define SPECIES_MOTH "moth"
|
||||
#define SPECIES_LIZARDMAN "lizardman"
|
||||
#define SPECIES_FELINID "felinid"
|
||||
|
||||
// Good
|
||||
#define SPECIES_MOTH "moth"
|
||||
#define SPECIES_LIZARDMAN "lizardman"
|
||||
#define SPECIES_FELINID "felinid"
|
||||
```
|
||||
|
||||
### No hacky code
|
||||
Hacky code, such as adding specific checks, is highly discouraged and only allowed when there is ***no*** other option. (Protip: 'I couldn't immediately think of a proper way so thus there must be no other option' is not gonna cut it here! If you can't think of anything else, say that outright and admit that you need help with it. Maintainers exist for exactly that reason.)
|
||||
Hacky code, such as adding specific checks, is highly discouraged and only allowed when there is ***no*** other option. (Protip: "I couldn't immediately think of a proper way so thus there must be no other option" is not gonna cut it here! If you can't think of anything else, say that outright and admit that you need help with it. Maintainers exist for exactly that reason.)
|
||||
|
||||
You can avoid hacky code by using object-oriented methodologies, such as overriding a function (called "procs" in DM) or sectioning code into functions and then overriding them as required.
|
||||
|
||||
@@ -158,7 +253,7 @@ There are two key points here:
|
||||
Remember: although this tradeoff makes sense in many cases, it doesn't cover them all. Think carefully about your addition before deciding if you need to use it.
|
||||
|
||||
### Prefer `Initialize()` over `New()` for atoms
|
||||
Our game controller is pretty good at handling long operations and lag, but it can't control what happens when the map is loaded, which calls `New` for all atoms on the map. If you're creating a new atom, use the `Initialize` proc to do what you would normally do in `New`. This cuts down on the number of proc calls needed when the world is loaded. See here for details on `Initialize`: https://github.com/tgstation/tgstation/blob/master/code/game/atoms.dm#L49
|
||||
Our game controller is pretty good at handling long operations and lag, but it can't control what happens when the map is loaded, which calls `New` for all atoms on the map. If you're creating a new atom, use the `Initialize` proc to do what you would normally do in `New`. This cuts down on the number of proc calls needed when the world is loaded. See here for details on `Initialize`: https://github.com/tgstation/tgstation/blob/34775d42a2db4e0f6734560baadcfcf5f5540910/code/game/atoms.dm#L166
|
||||
While we normally encourage (and in some cases, even require) bringing out of date code up to date when you make unrelated changes near the out of date code, that is not the case for `New` -> `Initialize` conversions. These systems are generally more dependant on parent and children procs so unrelated random conversions of existing things can cause bugs that take months to figure out.
|
||||
|
||||
### No magic numbers or strings
|
||||
@@ -187,7 +282,7 @@ This is clearer and enhances readability of your code! Get used to doing it!
|
||||
### Control statements
|
||||
(if, while, for, etc)
|
||||
|
||||
* All control statements must not contain code on the same line as the statement (`if (blah) return`)
|
||||
* No control statement may contain code on the same line as the statement (`if (blah) return`)
|
||||
* All control statements comparing a variable to a number should use the formula of `thing` `operator` `number`, not the reverse (eg: `if (count <= 10)` not `if (10 >= count)`)
|
||||
|
||||
### Use early return
|
||||
@@ -213,6 +308,97 @@ This is good:
|
||||
````
|
||||
This prevents nesting levels from getting deeper then they need to be.
|
||||
|
||||
### Use our time defines
|
||||
|
||||
The codebase contains some defines which will automatically multiply a number by the correct amount to get a number in deciseconds. Using these is preffered over using a literal amount in deciseconds.
|
||||
|
||||
The defines are as follows:
|
||||
* SECONDS
|
||||
* MINUTES
|
||||
* HOURS
|
||||
|
||||
This is bad:
|
||||
````DM
|
||||
/datum/datum1/proc/proc1()
|
||||
if(do_after(mob, 15))
|
||||
mob.dothing()
|
||||
````
|
||||
|
||||
This is good:
|
||||
````DM
|
||||
/datum/datum1/proc/proc1()
|
||||
if(do_after(mob, 1.5 SECONDS))
|
||||
mob.dothing()
|
||||
````
|
||||
|
||||
### Getters and setters
|
||||
|
||||
* Avoid getter procs. They are useful tools in languages with that properly enforce variable privacy and encapsulation, but DM is not one of them. The upfront cost in proc overhead is met with no benefits, and it may tempt to develop worse code.
|
||||
|
||||
This is bad:
|
||||
```DM
|
||||
/datum/datum1/proc/simple_getter()
|
||||
return gotten_variable
|
||||
```
|
||||
Prefer to either access the variable directly or use a macro/define.
|
||||
|
||||
|
||||
* Make usage of variables or traits, set up through condition setters, for a more maintainable alternative to compex and redefined getters.
|
||||
|
||||
These are bad:
|
||||
```DM
|
||||
/datum/datum1/proc/complex_getter()
|
||||
return condition ? VALUE_A : VALUE_B
|
||||
|
||||
/datum/datum1/child_datum/complex_getter()
|
||||
return condition ? VALUE_C : VALUE_D
|
||||
```
|
||||
|
||||
This is good:
|
||||
```DM
|
||||
/datum/datum1
|
||||
var/getter_turned_into_variable
|
||||
|
||||
/datum/datum1/proc/set_condition(new_value)
|
||||
if(condition == new_value)
|
||||
return
|
||||
condition = new_value
|
||||
on_condition_change()
|
||||
|
||||
/datum/datum1/proc/on_condition_change()
|
||||
getter_turned_into_variable = condition ? VALUE_A : VALUE_B
|
||||
|
||||
/datum/datum1/child_datum/on_condition_change()
|
||||
getter_turned_into_variable = condition ? VALUE_C : VALUE_D
|
||||
```
|
||||
|
||||
### Avoid unnecessary type checks and obscuring nulls in lists
|
||||
Typecasting in `for` loops carries an implied `istype()` check that filters non-matching types, nulls included. The `as anything` key can be used to skip the check.
|
||||
|
||||
If we know the list is supposed to only contain the desired type then we want to skip the check not only for the small optimization it offers, but also to catch any null entries that may creep into the list.
|
||||
|
||||
Nulls in lists tend to point to improperly-handled references, making hard deletes hard to debug. Generating a runtime in those cases is more often than not positive.
|
||||
|
||||
This is bad:
|
||||
```DM
|
||||
var/list/bag_of_atoms = list(new /obj, new /mob, new /atom, new /atom/movable, new /atom/movable)
|
||||
var/highest_alpha = 0
|
||||
for(var/atom/thing in bag_of_atoms)
|
||||
if(thing.alpha <= highest_alpha)
|
||||
continue
|
||||
highest_alpha = thing.alpha
|
||||
```
|
||||
|
||||
This is good:
|
||||
```DM
|
||||
var/list/bag_of_atoms = list(new /obj, new /mob, new /atom, new /atom/movable, new /atom/movable)
|
||||
var/highest_alpha = 0
|
||||
for(var/atom/thing as anything in bag_of_atoms)
|
||||
if(thing.alpha <= highest_alpha)
|
||||
continue
|
||||
highest_alpha = thing.alpha
|
||||
```
|
||||
|
||||
### Develop Secure Code
|
||||
|
||||
* Player input must always be escaped safely, we recommend you use stripped_input in all cases where you would use input. Essentially, just always treat input from players as inherently malicious and design with that use case in mind
|
||||
@@ -245,15 +431,24 @@ This prevents nesting levels from getting deeper then they need to be.
|
||||
|
||||
* Primary keys are inherently immutable and you must never do anything to change the primary key of a row or entity. This includes preserving auto increment numbers of rows when copying data to a table in a conversion script. No amount of bitching about gaps in ids or out of order ids will save you from this policy.
|
||||
|
||||
* The ttl for data from the database is 10 seconds. You must have a compelling reason to store and reuse data for longer then this.
|
||||
|
||||
* Do not write stored and transformed data to the database, instead, apply the transformation to the data in the database directly.
|
||||
* ie: SELECTing a number from the database, doubling it, then updating the database with the doubled number. If the data in the database changed between step 1 and 3, you'll get an incorrect result. Instead, directly double it in the update query. `UPDATE table SET num = num*2` instead of `UPDATE table SET num = [num]`.
|
||||
* if the transformation is user provided (such as allowing a user to edit a string), you should confirm the value being updated did not change in the database in the intervening time before writing the new user provided data by checking the old value with the current value in the database, and if it has changed, allow the user to decide what to do next.
|
||||
|
||||
### Mapping Standards
|
||||
* Adding, Removing, or Replacing Station Maps
|
||||
* All pull requests adding, removing, or replacing station maps must receive prior approval from a maptainer, or they will be closed without additional warning.
|
||||
|
||||
* TGM Format & Map Merge
|
||||
* All new maps submitted to the repo through a pull request must be in TGM format (unless there is a valid reason present to have it in the default BYOND format.) This is done using the [Map Merge](https://github.com/tgstation/tgstation/wiki/Map-Merger) utility included in the repo to convert the file to TGM format.
|
||||
* All new maps submitted to the repo through a pull request must be in TGM format (unless there is a valid reason present to have it in the default BYOND format). This is done using the [Map Merge](https://tgstation13.org/wiki/Map_Merger) utility included in the repo to convert the file to TGM format.
|
||||
* Likewise, you MUST run Map Merge prior to opening your PR when updating existing maps to minimize the change differences (even when using third party mapping programs such as FastDMM.)
|
||||
* Failure to run Map Merge on a map after using third party mapping programs (such as FastDMM) greatly increases the risk of the map's key dictionary becoming corrupted by future edits after running map merge. Resolving the corruption issue involves rebuilding the map's key dictionary; id est rewriting all the keys contained within the map by reconverting it from BYOND to TGM format - which creates very large differences that ultimately delay the PR process and is extremely likely to cause merge conflicts with other pull requests.
|
||||
|
||||
* Variable Editing (Var-edits)
|
||||
* While var-editing an item within the editor is perfectly fine, it is preferred that when you are changing the base behavior of an item (how it functions) that you make a new subtype of that item within the code, especially if you plan to use the item in multiple locations on the same map, or across multiple maps. This makes it easier to make corrections as needed to all instances of the item at one time as opposed to having to find each instance of it and change them all individually.
|
||||
* Subtypes only intended to be used on away mission or ruin maps should be contained within an .dm file with a name corresponding to that map within `code\modules\awaymissions` or `code\modules\ruins` respectively. This is so in the event that the map is removed, that subtype will be removed at the same time as well to minimize leftover/unused data within the repo.
|
||||
* Subtypes only intended to be used on away mission or ruin maps should be contained within a .dm file with a name corresponding to that map within `code\modules\awaymissions` or `code\modules\ruins` respectively. This is so in the event that the map is removed, that subtype will be removed at the same time as well to minimize leftover/unused data within the repo.
|
||||
* Please attempt to clean out any dirty variables that may be contained within items you alter through var-editing. For example, due to how DM functions, changing the `pixel_x` variable from 23 to 0 will leave a dirty record in the map's code of `pixel_x = 0`. Likewise this can happen when changing an item's icon to something else and then back. This can lead to some issues where an item's icon has changed within the code, but becomes broken on the map due to it still attempting to use the old entry.
|
||||
* Areas should not be var-edited on a map to change it's name or attributes. All areas of a single type and it's altered instances are considered the same area within the code, and editing their variables on a map can lead to issues with powernets and event subsystems which are difficult to debug.
|
||||
|
||||
@@ -264,6 +459,99 @@ This prevents nesting levels from getting deeper then they need to be.
|
||||
* [tgui/README.md](../tgui/README.md)
|
||||
* [tgui/tutorial-and-examples.md](../tgui/docs/tutorial-and-examples.md)
|
||||
|
||||
### Signal Handlers
|
||||
All procs that are registered to listen for signals using `RegisterSignal()` must contain at the start of the proc `SIGNAL_HANDLER` eg;
|
||||
```
|
||||
/type/path/proc/signal_callback()
|
||||
SIGNAL_HANDLER
|
||||
// rest of the code
|
||||
```
|
||||
This is to ensure that it is clear the proc handles signals and turns on a lint to ensure it does not sleep.
|
||||
|
||||
There exists `SIGNAL_HANDLER_DOES_SLEEP`, but this is only for legacy signal handlers that still sleep, new/changed code should not use this.
|
||||
|
||||
### Enforcing parent calling
|
||||
When adding new signals to root level procs, eg;
|
||||
```
|
||||
/atom/proc/setDir(newdir)
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
SEND_SIGNAL(src, COMSIG_ATOM_DIR_CHANGE, dir, newdir)
|
||||
dir = newdir
|
||||
```
|
||||
The `SHOULD_CALL_PARENT(TRUE)` lint should be added to ensure that overrides/child procs call the parent chain and ensure the signal is sent.
|
||||
|
||||
### Use descriptive and obvious names
|
||||
Optimize for readability, not writability. While it is certainly easier to write `M` than `victim`, it will cause issues down the line for other developers to figure out what exactly your code is doing, even if you think the variable's purpose is obvious.
|
||||
|
||||
#### Don't use abbreviations
|
||||
Avoid variables like C, M, and H. Prefer names like "user", "victim", "weapon", etc.
|
||||
|
||||
```dm
|
||||
// What is M? The user? The target?
|
||||
// What is A? The target? The item?
|
||||
/proc/use_item(mob/M, atom/A)
|
||||
|
||||
// Much better!
|
||||
/proc/use_item(mob/user, atom/target)
|
||||
```
|
||||
|
||||
Unless it is otherwise obvious, try to avoid just extending variables like "C" to "carbon"--this is slightly more helpful, but does not describe the *context* of the use of the variable.
|
||||
|
||||
#### Naming things when typecasting
|
||||
When typecasting, keep your names descriptive:
|
||||
```dm
|
||||
var/mob/living/living_target = target
|
||||
var/mob/living/carbon/carbon_target = living_target
|
||||
```
|
||||
|
||||
Of course, if you have a variable name that better describes the situation when typecasting, feel free to use it.
|
||||
|
||||
Note that it's okay, semantically, to use the same variable name as the type, e.g.:
|
||||
```dm
|
||||
var/atom/atom
|
||||
var/client/client
|
||||
var/mob/mob
|
||||
```
|
||||
|
||||
Your editor may highlight the variable names, but BYOND, and we, accept these as variable names:
|
||||
|
||||
```dm
|
||||
// This functions properly!
|
||||
var/client/client = CLIENT_FROM_VAR(usr)
|
||||
// vvv this may be highlighted, but it's fine!
|
||||
client << browse(...)
|
||||
```
|
||||
|
||||
#### Name things as directly as possible
|
||||
`was_called` is better than `has_been_called`. `notify` is better than `do_notification`.
|
||||
|
||||
#### Avoid negative variable names
|
||||
`is_flying` is better than `is_not_flying`. `late` is better than `not_on_time`.
|
||||
This prevents double-negatives (such as `if (!is_not_flying)` which can make complex checks more difficult to parse.
|
||||
|
||||
#### Exceptions to variable names
|
||||
|
||||
Exceptions can be made in the case of inheriting existing procs, as it makes it so you can use named parameters, but *new* variable names must follow these standards. It is also welcome, and encouraged, to refactor existing procs to use clearer variable names.
|
||||
|
||||
Naming numeral iterator variables `i` is also allowed, but do remember to [Avoid unnecessary type checks and obscuring nulls in lists](#avoid-unnecessary-type-checks-and-obscuring-nulls-in-lists), and making more descriptive variables is always encouraged.
|
||||
|
||||
```dm
|
||||
// Bad
|
||||
for (var/datum/reagent/R as anything in reagents)
|
||||
|
||||
// Good
|
||||
for (var/datum/reagent/deadly_reagent as anything in reagents)
|
||||
|
||||
// Allowed, but still has the potential to not be clear. What does `i` refer to?
|
||||
for (var/i in 1 to 12)
|
||||
|
||||
// Better
|
||||
for (var/month in 1 to 12)
|
||||
|
||||
// Bad, only use `i` for numeral loops
|
||||
for (var/i in reagents)
|
||||
```
|
||||
|
||||
### Other Notes
|
||||
* Code should be modular where possible; if you are working on a new addition, then strongly consider putting it in its own file unless it makes sense to put it with similar ones (i.e. a new tool would go in the "tools.dm" file)
|
||||
|
||||
@@ -273,6 +561,8 @@ This prevents nesting levels from getting deeper then they need to be.
|
||||
|
||||
* Do not divide when you can easily convert it to multiplication. (ie `4/2` should be done as `4*0.5`)
|
||||
|
||||
* Separating single lines into more readable blocks is not banned, however you should use it only where it makes new information more accessible, or aids maintainability. We do not have a column limit, and mass conversions will not be received well.
|
||||
|
||||
* If you used regex to replace code during development of your code, post the regex in your PR for the benefit of future developers and downstream users.
|
||||
|
||||
* Changes to the `/config` tree must be made in a way that allows for updating server deployments while preserving previous behaviour. This is due to the fact that the config tree is to be considered owned by the user and not necessarily updated alongside the remainder of the code. The code to preserve previous behaviour may be removed at some point in the future given the OK by maintainers.
|
||||
@@ -306,77 +596,43 @@ Math operators like +, -, /, *, etc are up in the air, just choose which version
|
||||
|
||||
#### Use
|
||||
* Bitwise AND - '&'
|
||||
* Should be written as ```bitfield & bitflag``` NEVER ```bitflag & bitfield```, both are valid, but the latter is confusing and nonstandard.
|
||||
* Should be written as `variable & CONSTANT` NEVER `CONSTANT & variable`. Both are valid, but the latter is confusing and nonstandard.
|
||||
* Associated lists declarations must have their key value quoted if it's a string
|
||||
* WRONG: list(a = "b")
|
||||
* RIGHT: list("a" = "b")
|
||||
* WRONG: `list(a = "b")`
|
||||
* RIGHT: `list("a" = "b")`
|
||||
|
||||
### Dream Maker Quirks/Tricks
|
||||
Like all languages, Dream Maker has its quirks, some of them are beneficial to us, like these
|
||||
|
||||
#### In-To for-loops
|
||||
```for(var/i = 1, i <= some_value, i++)``` is a fairly standard way to write an incremental for loop in most languages (especially those in the C family), but DM's ```for(var/i in 1 to some_value)``` syntax is oddly faster than its implementation of the former syntax; where possible, it's advised to use DM's syntax. (Note, the ```to``` keyword is inclusive, so it automatically defaults to replacing ```<=```; if you want ```<``` then you should write it as ```1 to some_value-1```).
|
||||
`for(var/i = 1, i <= some_value, i++)` is a fairly standard way to write an incremental for loop in most languages (especially those in the C family), but DM's `for(var/i in 1 to some_value)` syntax is oddly faster than its implementation of the former syntax; where possible, it's advised to use DM's syntax. (Note, the `to` keyword is inclusive, so it automatically defaults to replacing `<=`; if you want `<` then you should write it as `1 to some_value-1`).
|
||||
|
||||
HOWEVER, if either ```some_value``` or ```i``` changes within the body of the for (underneath the ```for(...)``` header) or if you are looping over a list AND changing the length of the list then you can NOT use this type of for-loop!
|
||||
HOWEVER, if either `some_value` or `i` changes within the body of the for (underneath the `for(...)` header) or if you are looping over a list AND changing the length of the list then you can NOT use this type of for-loop!
|
||||
|
||||
### for(var/A in list) VS for(var/i in 1 to list.len)
|
||||
#### `for(var/A in list)` versus `for(var/i in 1 to list.len)`
|
||||
The former is faster than the latter, as shown by the following profile results:
|
||||
https://file.house/zy7H.png
|
||||
Code used for the test in a readable format:
|
||||
https://pastebin.com/w50uERkG
|
||||
|
||||
|
||||
#### Istypeless for loops
|
||||
A name for a differing syntax for writing for-each style loops in DM. It's NOT DM's standard syntax, hence why this is considered a quirk. Take a look at this:
|
||||
```DM
|
||||
var/list/bag_of_items = list(sword, apple, coinpouch, sword, sword)
|
||||
var/obj/item/sword/best_sword
|
||||
for(var/obj/item/sword/S in bag_of_items)
|
||||
if(!best_sword || S.damage > best_sword.damage)
|
||||
best_sword = S
|
||||
```
|
||||
The above is a simple proc for checking all swords in a container and returning the one with the highest damage, and it uses DM's standard syntax for a for-loop by specifying a type in the variable of the for's header that DM interprets as a type to filter by. It performs this filter using ```istype()``` (or some internal-magic similar to ```istype()``` - this is BYOND, after all). This is fine in its current state for ```bag_of_items```, but if ```bag_of_items``` contained ONLY swords, or only SUBTYPES of swords, then the above is inefficient. For example:
|
||||
```DM
|
||||
var/list/bag_of_swords = list(sword, sword, sword, sword)
|
||||
var/obj/item/sword/best_sword
|
||||
for(var/obj/item/sword/S in bag_of_swords)
|
||||
if(!best_sword || S.damage > best_sword.damage)
|
||||
best_sword = S
|
||||
```
|
||||
specifies a type for DM to filter by.
|
||||
|
||||
With the previous example that's perfectly fine, we only want swords, but here the bag only contains swords? Is DM still going to try to filter because we gave it a type to filter by? YES, and here comes the inefficiency. Wherever a list (or other container, such as an atom (in which case you're technically accessing their special contents list, but that's irrelevant)) contains datums of the same datatype or subtypes of the datatype you require for your loop's body,
|
||||
you can circumvent DM's filtering and automatic ```istype()``` checks by writing the loop as such:
|
||||
```DM
|
||||
var/list/bag_of_swords = list(sword, sword, sword, sword)
|
||||
var/obj/item/sword/best_sword
|
||||
for(var/s in bag_of_swords)
|
||||
var/obj/item/sword/S = s
|
||||
if(!best_sword || S.damage > best_sword.damage)
|
||||
best_sword = S
|
||||
```
|
||||
Of course, if the list contains data of a mixed type then the above optimisation is DANGEROUS, as it will blindly typecast all data in the list as the specified type, even if it isn't really that type, causing runtime errors.
|
||||
|
||||
#### Dot variable
|
||||
Like other languages in the C family, DM has a ```.``` or "Dot" operator, used for accessing variables/members/functions of an object instance.
|
||||
Like other languages in the C family, DM has a `.` or "Dot" operator, used for accessing variables/members/functions of an object instance.
|
||||
eg:
|
||||
```DM
|
||||
var/mob/living/carbon/human/H = YOU_THE_READER
|
||||
H.gib()
|
||||
```
|
||||
However, DM also has a dot variable, accessed just as ```.``` on its own, defaulting to a value of null. Now, what's special about the dot operator is that it is automatically returned (as in the ```return``` statement) at the end of a proc, provided the proc does not already manually return (```return count``` for example.) Why is this special?
|
||||
However, DM also has a dot variable, accessed just as `.` on its own, defaulting to a value of null. Now, what's special about the dot operator is that it is automatically returned (as in the `return` statement) at the end of a proc, provided the proc does not already manually return (`return count` for example.) Why is this special?
|
||||
|
||||
With ```.``` being everpresent in every proc, can we use it as a temporary variable? Of course we can! However, the ```.``` operator cannot replace a typecasted variable - it can hold data any other var in DM can, it just can't be accessed as one, although the ```.``` operator is compatible with a few operators that look weird but work perfectly fine, such as: ```.++``` for incrementing ```.'s``` value, or ```.[1]``` for accessing the first element of ```.```, provided that it's a list.
|
||||
With `.` being everpresent in every proc, can we use it as a temporary variable? Of course we can! However, the `.` operator cannot replace a typecasted variable - it can hold data any other var in DM can, it just can't be accessed as one, although the `.` operator is compatible with a few operators that look weird but work perfectly fine, such as: `.++` for incrementing `.'s` value, or `.[1]` for accessing the first element of `.`, provided that it's a list.
|
||||
|
||||
## Globals versus static
|
||||
### Globals versus static
|
||||
|
||||
DM has a var keyword, called global. This var keyword is for vars inside of types. For instance:
|
||||
|
||||
```DM
|
||||
mob
|
||||
var
|
||||
global
|
||||
thing = TRUE
|
||||
/mob
|
||||
var/global/thing = TRUE
|
||||
```
|
||||
This does NOT mean that you can access it everywhere like a global var. Instead, it means that that var will only exist once for all instances of its type, in this case that var will only exist once for all mobs - it's shared across everything in its type. (Much more like the keyword `static` in other languages like PHP/C++/C#/Java)
|
||||
|
||||
@@ -388,11 +644,13 @@ There is also an undocumented keyword called `static` that has the same behaviou
|
||||
|
||||
There is no strict process when it comes to merging pull requests. Pull requests will sometimes take a while before they are looked at by a maintainer; the bigger the change, the more time it will take before they are accepted into the code. Every team member is a volunteer who is giving up their own time to help maintain and contribute, so please be courteous and respectful. Here are some helpful ways to make it easier for you and for the maintainers when making a pull request.
|
||||
|
||||
* Make sure your pull request complies to the requirements outlined in [this guide](http://tgstation13.org/wiki/Getting_Your_Pull_Accepted)
|
||||
* Make sure your pull request complies to the requirements outlined here
|
||||
|
||||
* You are going to be expected to document all your changes in the pull request. Failing to do so will mean delaying it as we will have to question why you made the change. On the other hand, you can speed up the process by making the pull request readable and easy to understand, with diagrams or before/after data.
|
||||
* You are expected to have tested your pull requests if it is anything that would warrant testing. Text only changes, single number balance changes, and similar generally don't need testing, but anything else does. This means by extension web edits are disallowed for larger changes.
|
||||
|
||||
* We ask that you use the changelog system to document your change, which prevents our players from being caught unaware by changes - you can find more information about this [on this wiki page](http://tgstation13.org/wiki/Guide_to_Changelogs).
|
||||
* You are going to be expected to document all your changes in the pull request. Failing to do so will mean delaying it as we will have to question why you made the change. On the other hand, you can speed up the process by making the pull request readable and easy to understand, with diagrams or before/after data. Should you be optimizing a routine you must provide proof by way of profiling that your changes are faster.
|
||||
|
||||
* We ask that you use the changelog system to document your player facing changes, which prevents our players from being caught unaware by said changes - you can find more information about this [on this wiki page](http://tgstation13.org/wiki/Guide_to_Changelogs).
|
||||
|
||||
* If you are proposing multiple changes, which change many different aspects of the code, you are expected to section them off into different pull requests in order to make it easier to review them and to deny/accept the changes that are deemed acceptable.
|
||||
|
||||
@@ -400,10 +658,12 @@ There is no strict process when it comes to merging pull requests. Pull requests
|
||||
|
||||
* Please explain why you are submitting the pull request, and how you think your change will be beneficial to the game. Failure to do so will be grounds for rejecting the PR.
|
||||
|
||||
* If your pull request is not finished make sure it is at least testable in a live environment. Pull requests that do not at least meet this requirement will be closed. You may request a maintainer reopen the pull request when you're ready, or make a new one.
|
||||
* If your pull request is not finished, you may open it as a draft for potential review. If you open it as a full-fledged PR make sure it is at least testable in a live environment. Pull requests that do not at least meet this requirement will be closed. You may request a maintainer reopen the pull request when you're ready, or make a new one.
|
||||
|
||||
* While we have no issue helping contributors (and especially new contributors) bring reasonably sized contributions up to standards via the pull request review process, larger contributions are expected to pass a higher bar of completeness and code quality *before* you open a pull request. Maintainers may close such pull requests that are deemed to be substantially flawed. You should take some time to discuss with maintainers or other contributors on how to improve the changes.
|
||||
|
||||
* After leaving reviews on an open pull request, maintainers may convert it to a draft. Once you have addressed all their comments to the best of your ability, feel free to mark the pull as `Ready for Review` again.
|
||||
|
||||
## Porting features/sprites/sounds/tools from other codebases
|
||||
|
||||
If you are porting features/tools from other codebases, you must give them credit where it's due. Typically, crediting them in your pull request and the changelog is the recommended way of doing it. Take note of what license they use though, porting stuff from AGPLv3 and GPLv3 codebases are allowed.
|
||||
@@ -413,19 +673,14 @@ Regarding sprites & sounds, you must credit the artist and possibly the codebase
|
||||
## Banned content
|
||||
Do not add any of the following in a Pull Request or risk getting the PR closed:
|
||||
* National Socialist Party of Germany content, National Socialist Party of Germany related content, or National Socialist Party of Germany references
|
||||
* Code where one line of code is split across mutiple lines (except for multiple, separate strings and comments; in those cases, existing longer lines must not be split up)
|
||||
* Code adding, removing, or updating the availability of alien races/species/human mutants without prior approval. Pull requests attempting to add or remove features from said races/species/mutants require prior approval as well.
|
||||
* Code which violates GitHub's [terms of service](https://github.com/site/terms).
|
||||
|
||||
Just because something isn't on this list doesn't mean that it's acceptable. Use common sense above all else.
|
||||
|
||||
## Content requiring prior approval
|
||||
Certain types of changes may require prior approval from maintainers. This currently includes:
|
||||
|
||||
* Code adding, removing, or updating the availability of alien races/species/human mutants. This includes pull requests attempting to add or remove features from said races/species/mutants. (Requires approval from at least one maintainer)
|
||||
* Code adding, removing, or modifying the functionality of adult-oriented features (such as, but not limited to: vore, genitals, MKUltra, and more). This also includes pull requests attempting to add or remove these features outright. (Requires approval from at least half of the formal maintainer team)
|
||||
|
||||
The above content requires approval from the specified amount of maintainers prior to PR creation. Seeking approval must be done via a @Maintainer ping in a relevant development/code or staff channel on the Discord, otherwise it will be considered insufficient. If a PR contains any of the above content, but the creator does not have sufficient approval prior to the PR's creation, then the PR may be closed by any maintainer, at any time, for any reason.
|
||||
|
||||
## A word on Git
|
||||
Yes, we know that the files have a tonne of mixed Windows and Linux line endings. Attempts to fix this have been met with less than stellar success, and as such we have decided to give up caring until there comes a time when it matters.
|
||||
This repository uses `LF` line endings for all code as specified in the **.gitattributes** and **.editorconfig** files.
|
||||
|
||||
Therefore, EOF settings of main repo are forbidden territory one must avoid wandering into, at risk of losing body and/or mind to the Git gods.
|
||||
Unless overridden or a non standard git binary is used the line ending settings should be applied to your clone automatically.
|
||||
|
||||
Note: VSC requires an [extension](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) to take advantage of editorconfig.
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## DOWNLOADING
|
||||
There are a number of ways to download the source code. Some are described here, an alternative all-inclusive guide is also located at https://www.tgstation13.org/wiki/Downloading_the_source_code
|
||||
|
||||
Option 1:
|
||||
Follow this: https://www.tgstation13.org/wiki/Setting_up_git, only with Citadel Station's repository instead of /tg/'s.
|
||||
|
||||
Option 2: Download the source code as a zip by clicking the ZIP button in the
|
||||
code tab of https://github.com/Citadel-Station-13/Citadel-Station-13
|
||||
(note: this will use a lot of bandwidth if you wish to update and is a lot of
|
||||
hassle if you want to make any changes at all, so it's not recommended.)
|
||||
@@ -0,0 +1,95 @@
|
||||
# INSTALLATION
|
||||
First-time installation should be fairly straightforward. First, you'll need
|
||||
BYOND installed. You can get it from https://www.byond.com/download. Once you've done
|
||||
that, extract the game files to wherever you want to keep them. This is a
|
||||
sourcecode-only release, so the next step is to compile the server files.
|
||||
|
||||
Double-click `BUILD.bat` in the root directory of the source code. This'll take
|
||||
a little while, and if everything's done right you'll get a message like this:
|
||||
|
||||
```
|
||||
saving tgstation.dmb (DEBUG mode)
|
||||
tgstation.dmb - 0 errors, 0 warnings
|
||||
```
|
||||
|
||||
If you see any errors or warnings, something has gone wrong - possibly a corrupt
|
||||
download or the files extracted wrong. If problems persist, ask for assistance
|
||||
in irc://irc.rizon.net/coderbus
|
||||
|
||||
Once that's done, open up the config folder. You'll want to edit config.txt to
|
||||
set the probabilities for different gamemodes in Secret and to set your server
|
||||
location so that all your players don't get disconnected at the end of each
|
||||
round. It's recommended you don't turn on the gamemodes with probability 0,
|
||||
except Extended, as they have various issues and aren't currently being tested,
|
||||
so they may have unknown and bizarre bugs. Extended is essentially no mode, and
|
||||
isn't in the Secret rotation by default as it's just not very fun.
|
||||
|
||||
You'll also want to edit config/admins.txt to remove the default admins and add
|
||||
your own. "Game Master" is the highest level of access, and probably the one
|
||||
you'll want to use for now. You can set up your own ranks and find out more in
|
||||
config/admin_ranks.txt
|
||||
|
||||
The format is
|
||||
|
||||
```
|
||||
byondkey = Rank
|
||||
```
|
||||
|
||||
where the admin rank must be properly capitalised.
|
||||
|
||||
This codebase also depends on a native library called rust-g. A precompiled
|
||||
Windows DLL is included in this repository, but Linux users will need to build
|
||||
and install it themselves. Directions can be found at the [rust-g
|
||||
repo](https://github.com/tgstation/rust-g).
|
||||
|
||||
Finally, to start the server, run Dream Daemon and enter the path to your
|
||||
compiled tgstation.dmb file. Make sure to set the port to the one you
|
||||
specified in the config.txt, and set the Security box to 'Safe'. Then press GO
|
||||
and the server should start up and be ready to join. It is also recommended that
|
||||
you set up the SQL backend (see below).
|
||||
|
||||
## UPDATING
|
||||
|
||||
To update an existing installation, first back up your /config and /data folders
|
||||
as these store your server configuration, player preferences and banlist.
|
||||
|
||||
Then, extract the new files (preferably into a clean directory, but updating in
|
||||
place should work fine), copy your /config and /data folders back into the new
|
||||
install, overwriting when prompted except if we've specified otherwise, and
|
||||
recompile the game. Once you start the server up again, you should be running
|
||||
the new version.
|
||||
|
||||
## HOSTING
|
||||
|
||||
If you'd like a more robust server hosting option for tgstation and its
|
||||
derivatives. Check out our server tools suite at
|
||||
https://github.com/tgstation/tgstation-server
|
||||
|
||||
If you decide to go this route, here are /tg/ specific details on hosting with TGS.
|
||||
|
||||
- We have two directories which should be setup in the instance's `Configuration/GameStaticFiles` directory:
|
||||
- `config` should be where you place your production configuration. Overwrites the default contents of the repo's [config](../config) directory.
|
||||
- `data` should be initially created as an empty directory. The game stores persistent data here.
|
||||
- You should incorporate our [custom build scripts for TGS4](../tools/tgs4_scripts) in the instance's `Configuration/EventScripts` directory. These handle including TGUI in the build and setting up rust-g on Linux.
|
||||
- Deployment security level must be set to `Trusted` or it will likely fail due to our native library usage.
|
||||
- We highly recommend using the BYOND version specified in [dependencies.sh](../dependencies.sh) to avoid potential unrecorded issues.
|
||||
|
||||
## SQL SETUP
|
||||
|
||||
The SQL backend requires a Mariadb server running 10.2 or later. Mysql is not supported but Mariadb is a drop in replacement for mysql. SQL is required for the library, stats tracking, admin notes, and job-only bans, among other features, mostly related to server administration. Your server details go in /config/dbconfig.txt, and the SQL schema is in /SQL/tgstation_schema.sql and /SQL/tgstation_schema_prefix.sql depending on if you want table prefixes. More detailed setup instructions are located here: https://www.tgstation13.org/wiki/Downloading_the_source_code#Setting_up_the_database
|
||||
|
||||
If you are hosting a testing server on windows you can use a standalone version of MariaDB pre load with a blank (but initialized) tgdb database. Find them here: https://tgstation13.download/database/ Just unzip and run for a working (but insecure) database server. Includes a zipped copy of the data folder for easy resetting back to square one.
|
||||
|
||||
## WEB/CDN RESOURCE DELIVERY
|
||||
|
||||
Web delivery of game resources makes it quicker for players to join and reduces some of the stress on the game server.
|
||||
|
||||
1. Edit compile_options.dm to set the `PRELOAD_RSC` define to `0`
|
||||
1. Add a url to config/external_rsc_urls pointing to a .zip file containing the .rsc.
|
||||
* If you keep up to date with /tg/ you could reuse /tg/'s rsc cdn at http://tgstation13.download/byond/tgstation.zip. Otherwise you can use cdn services like CDN77 or cloudflare (requires adding a page rule to enable caching of the zip), or roll your own cdn using route 53 and vps providers.
|
||||
* Regardless even offloading the rsc to a website without a CDN will be a massive improvement over the in game system for transferring files.
|
||||
|
||||
## IRC BOT SETUP
|
||||
|
||||
Included in the repository is a python3 compatible IRC bot capable of relaying adminhelps to a specified
|
||||
IRC channel/server, see the /tools/minibot folder for more
|
||||
@@ -0,0 +1,20 @@
|
||||
no_balance_label = "GBP: No Update"
|
||||
reset_label = "GBP: Reset"
|
||||
|
||||
[points]
|
||||
"Atomic" = 2
|
||||
"Balance/Rebalance" = -8
|
||||
"Code Improvement" = 2
|
||||
"Feature" = -10
|
||||
"Feedback" = 2
|
||||
"Fix" = 3
|
||||
"Grammar and Formatting" = 1
|
||||
"Logging" = 1
|
||||
"Performance" = 12
|
||||
"Priority: CRITICAL" = 20
|
||||
"Priority: High" = 15
|
||||
"Quality of Life" = 1
|
||||
"Refactor" = 10
|
||||
"Sound" = 3
|
||||
"Sprites" = 3
|
||||
"Unit Tests" = 6
|
||||
@@ -58,8 +58,9 @@ jobs:
|
||||
bash tools/ci/install_byond.sh
|
||||
source $HOME/BYOND/byond/bin/byondsetup
|
||||
python3 tools/ci/template_dm_generator.py
|
||||
tgui/bin/tgui --build
|
||||
bash tools/ci/dm.sh -DCIBUILDING -DCITESTING -DALL_MAPS tgstation.dme
|
||||
tools/build/build
|
||||
env:
|
||||
CBT_BUILD_MODE : ALL_MAPS
|
||||
|
||||
run_all_tests:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
@@ -97,9 +98,10 @@ jobs:
|
||||
run: |
|
||||
bash tools/ci/install_byond.sh
|
||||
source $HOME/BYOND/byond/bin/byondsetup
|
||||
tgui/bin/tgui --build
|
||||
bash tools/ci/dm.sh -DCIBUILDING tgstation.dme
|
||||
# bash tools/ci/run_server.sh
|
||||
tools/build/build
|
||||
# bash tools/ci/run_server.sh
|
||||
env:
|
||||
CBT_BUILD_MODE: TEST_RUN
|
||||
|
||||
test_windows:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
@@ -109,6 +111,8 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Compile
|
||||
run: pwsh tools/ci/build.ps1
|
||||
env:
|
||||
DM_EXE: "C:\\byond\\bin\\dm.exe"
|
||||
- name: Create artifact
|
||||
run: |
|
||||
md deploy
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// tgstation-server DMAPI
|
||||
|
||||
#define TGS_DMAPI_VERSION "5.2.9"
|
||||
#define TGS_DMAPI_VERSION "6.0.3"
|
||||
|
||||
// All functions and datums outside this document are subject to change with any version and should not be relied on.
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
#define TGS_EVENT_REPO_CHECKOUT 1
|
||||
/// When the repository performs a fetch operation. No parameters
|
||||
#define TGS_EVENT_REPO_FETCH 2
|
||||
/// When the repository merges a pull request. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user
|
||||
/// When the repository test merges. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user
|
||||
#define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3
|
||||
/// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path
|
||||
#define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4
|
||||
@@ -95,8 +95,13 @@
|
||||
#define TGS_EVENT_WATCHDOG_SHUTDOWN 15
|
||||
/// Before the watchdog detaches for a TGS update/restart. No parameters.
|
||||
#define TGS_EVENT_WATCHDOG_DETACH 16
|
||||
// We don't actually implement this value as the DMAPI can never receive it
|
||||
// We don't actually implement these 4 events as the DMAPI can never receive them.
|
||||
// #define TGS_EVENT_WATCHDOG_LAUNCH 17
|
||||
// #define TGS_EVENT_WATCHDOG_CRASH 18
|
||||
// #define TGS_EVENT_WORLD_END_PROCESS 19
|
||||
// #define TGS_EVENT_WORLD_REBOOT 20
|
||||
/// Watchdog event when TgsInitializationComplete() is called. No parameters.
|
||||
#define TGS_EVENT_WORLD_PRIME 21
|
||||
|
||||
// OTHER ENUMS
|
||||
|
||||
@@ -130,7 +135,6 @@
|
||||
*
|
||||
* This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running.
|
||||
* Afterwards, consider explicitly setting it to what you want to avoid this BYOND bug: http://www.byond.com/forum/post/2575184
|
||||
* Before this point, note that any static files or directories may be in use by another server. Your code should account for this.
|
||||
* This function should not be called before ..() in [/world/proc/New].
|
||||
*/
|
||||
/world/proc/TgsInitializationComplete()
|
||||
@@ -140,7 +144,7 @@
|
||||
#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return
|
||||
|
||||
/**
|
||||
* Call this at the beginning of [world/proc/Reboot].
|
||||
* Call this as late as possible in [world/proc/Reboot].
|
||||
*/
|
||||
/world/proc/TgsReboot()
|
||||
return
|
||||
@@ -152,6 +156,8 @@
|
||||
/datum/tgs_revision_information
|
||||
/// Full SHA of the commit.
|
||||
var/commit
|
||||
/// ISO 8601 timestamp of when the commit was created
|
||||
var/timestamp
|
||||
/// Full sha of last known remote commit. This may be null if the TGS repository is not currently tracking a remote branch.
|
||||
var/origin_commit
|
||||
|
||||
@@ -190,21 +196,19 @@
|
||||
|
||||
/// Represents a merge of a GitHub pull request.
|
||||
/datum/tgs_revision_information/test_merge
|
||||
/// The pull request number.
|
||||
/// The test merge number.
|
||||
var/number
|
||||
/// The pull request title when it was merged.
|
||||
/// The test merge source's title when it was merged.
|
||||
var/title
|
||||
/// The pull request body when it was merged.
|
||||
/// The test merge source's body when it was merged.
|
||||
var/body
|
||||
/// The GitHub username of the pull request's author.
|
||||
/// The Username of the test merge source's author.
|
||||
var/author
|
||||
/// An http URL to the pull request.
|
||||
/// An http URL to the test merge source.
|
||||
var/url
|
||||
/// The SHA of the pull request when that was merged.
|
||||
var/pull_request_commit
|
||||
/// ISO 8601 timestamp of when the pull request was merged.
|
||||
var/time_merged
|
||||
/// (Nullable) Comment left by the TGS user who initiated the merge..
|
||||
/// The SHA of the test merge when that was merged.
|
||||
var/head_commit
|
||||
/// Optional comment left by the TGS user who initiated the merge.
|
||||
var/comment
|
||||
|
||||
/// Represents a connected chat channel.
|
||||
@@ -263,11 +267,11 @@
|
||||
// API FUNCTIONS
|
||||
|
||||
/// Returns the maximum supported [/datum/tgs_version] of the DMAPI.
|
||||
/world/proc/TgsMaximumAPIVersion()
|
||||
/world/proc/TgsMaximumApiVersion()
|
||||
return
|
||||
|
||||
/// Returns the minimum supported [/datum/tgs_version] of the DMAPI.
|
||||
/world/proc/TgsMinimumAPIVersion()
|
||||
/world/proc/TgsMinimumApiVersion()
|
||||
return
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/*!
|
||||
* Copyright (c) 2020 Aleksej Komarov
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* tgui subsystem
|
||||
*
|
||||
* Contains all tgui state and subsystem code.
|
||||
*
|
||||
* Copyright (c) 2020 Aleksej Komarov
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
SUBSYSTEM_DEF(tgui)
|
||||
@@ -42,8 +45,8 @@ SUBSYSTEM_DEF(tgui)
|
||||
var/datum/tgui/ui = current_run[current_run.len]
|
||||
current_run.len--
|
||||
// TODO: Move user/src_object check to process()
|
||||
if(ui && ui.user && ui.src_object)
|
||||
ui.process()
|
||||
if(ui?.user && ui.src_object)
|
||||
ui.process(wait * 0.1)
|
||||
else
|
||||
open_uis.Remove(ui)
|
||||
if(MC_TICK_CHECK)
|
||||
@@ -191,8 +194,8 @@ SUBSYSTEM_DEF(tgui)
|
||||
return count
|
||||
for(var/datum/tgui/ui in open_uis_by_src[key])
|
||||
// Check if UI is valid.
|
||||
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user))
|
||||
ui.process(force = 1)
|
||||
if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user))
|
||||
ui.process(wait * 0.1, force = 1)
|
||||
count++
|
||||
return count
|
||||
|
||||
@@ -213,7 +216,7 @@ SUBSYSTEM_DEF(tgui)
|
||||
return count
|
||||
for(var/datum/tgui/ui in open_uis_by_src[key])
|
||||
// Check if UI is valid.
|
||||
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user))
|
||||
if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user))
|
||||
ui.close()
|
||||
count++
|
||||
return count
|
||||
@@ -230,7 +233,7 @@ SUBSYSTEM_DEF(tgui)
|
||||
for(var/key in open_uis_by_src)
|
||||
for(var/datum/tgui/ui in open_uis_by_src[key])
|
||||
// Check if UI is valid.
|
||||
if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user))
|
||||
if(ui?.src_object && ui.user && ui.src_object.ui_host(ui.user))
|
||||
ui.close()
|
||||
count++
|
||||
return count
|
||||
@@ -251,7 +254,7 @@ SUBSYSTEM_DEF(tgui)
|
||||
return count
|
||||
for(var/datum/tgui/ui in user.tgui_open_uis)
|
||||
if(isnull(src_object) || ui.src_object == src_object)
|
||||
ui.process(force = 1)
|
||||
ui.process(wait * 0.1, force = 1)
|
||||
count++
|
||||
return count
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
if(revinfo)
|
||||
commit = revinfo.commit
|
||||
originmastercommit = revinfo.origin_commit
|
||||
date = rustg_git_commit_date(commit)
|
||||
date = revinfo.timestamp || rustg_git_commit_date(commit)
|
||||
|
||||
// goes to DD log and config_error.txt
|
||||
log_world(get_log_message())
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
for(var/line in testmerge)
|
||||
var/datum/tgs_revision_information/test_merge/tm = line
|
||||
msg += "Test merge active of PR #[tm.number] commit [tm.pull_request_commit]"
|
||||
SSblackbox.record_feedback("associative", "testmerged_prs", 1, list("number" = "[tm.number]", "commit" = "[tm.pull_request_commit]", "title" = "[tm.title]", "author" = "[tm.author]"))
|
||||
msg += "Test merge active of PR #[tm.number] commit [tm.head_commit]"
|
||||
SSblackbox.record_feedback("associative", "testmerged_prs", 1, list("number" = "[tm.number]", "commit" = "[tm.head_commit]", "title" = "[tm.title]", "author" = "[tm.author]"))
|
||||
|
||||
if(commit && commit != originmastercommit)
|
||||
msg += "HEAD: [commit]"
|
||||
@@ -45,7 +45,7 @@
|
||||
. = header ? "The following pull requests are currently test merged:<br>" : ""
|
||||
for(var/line in testmerge)
|
||||
var/datum/tgs_revision_information/test_merge/tm = line
|
||||
var/cm = tm.pull_request_commit
|
||||
var/cm = tm.head_commit
|
||||
var/details = ": '" + html_encode(tm.title) + "' by " + html_encode(tm.author) + " at commit " + html_encode(copytext_char(cm, 1, 11))
|
||||
if(details && findtext(details, "\[s\]") && (!usr || !usr.client.holder))
|
||||
continue
|
||||
|
||||
@@ -166,6 +166,13 @@
|
||||
)
|
||||
parents = list("font-awesome.css" = 'html/font-awesome/css/all.min.css')
|
||||
|
||||
/datum/asset/simple/namespaced/tgfont
|
||||
assets = list(
|
||||
"tgfont.eot" = 'tgui/packages/tgfont/dist/tgfont.eot',
|
||||
"tgfont.woff2" = 'tgui/packages/tgfont/dist/tgfont.woff2',
|
||||
)
|
||||
parents = list("tgfont.css" = 'tgui/packages/tgfont/dist/tgfont.css')
|
||||
|
||||
/datum/asset/spritesheet/chat
|
||||
name = "chat"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
if(5)
|
||||
api_datum = /datum/tgs_api/v5
|
||||
|
||||
var/datum/tgs_version/max_api_version = TgsMaximumAPIVersion();
|
||||
var/datum/tgs_version/max_api_version = TgsMaximumApiVersion();
|
||||
if(version.suite != null && version.minor != null && version.patch != null && version.deprecated_patch != null && version.deprefixed_parameter > max_api_version.deprefixed_parameter)
|
||||
TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
|
||||
api_datum = /datum/tgs_api/latest
|
||||
@@ -64,10 +64,10 @@
|
||||
TGS_WRITE_GLOBAL(tgs, null)
|
||||
TGS_ERROR_LOG("Failed to activate API!")
|
||||
|
||||
/world/TgsMaximumAPIVersion()
|
||||
/world/TgsMaximumApiVersion()
|
||||
return new /datum/tgs_version("5.x.x")
|
||||
|
||||
/world/TgsMinimumAPIVersion()
|
||||
/world/TgsMinimumApiVersion()
|
||||
return new /datum/tgs_version("3.2.x")
|
||||
|
||||
/world/TgsInitializationComplete()
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
comms_key = world.params[SERVICE_WORLD_PARAM]
|
||||
instance_name = world.params[SERVICE_INSTANCE_PARAM]
|
||||
if(!instance_name)
|
||||
instance_name = "TG Station Server" //maybe just upgraded
|
||||
instance_name = "TG Station Server" //maybe just upgraded
|
||||
|
||||
var/list/logs = file2list(".git/logs/HEAD")
|
||||
if(logs.len)
|
||||
@@ -92,14 +92,14 @@
|
||||
if(skip_compat_check && !fexists(SERVICE_INTERFACE_DLL))
|
||||
TGS_ERROR_LOG("Service parameter present but no interface DLL detected. This is symptomatic of running a service less than version 3.1! Please upgrade.")
|
||||
return
|
||||
call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval
|
||||
call(SERVICE_INTERFACE_DLL, SERVICE_INTERFACE_FUNCTION)(instance_name, command) //trust no retval
|
||||
return TRUE
|
||||
|
||||
/datum/tgs_api/v3210/OnTopic(T)
|
||||
var/list/params = params2list(T)
|
||||
var/their_sCK = params[SERVICE_CMD_PARAM_KEY]
|
||||
if(!their_sCK)
|
||||
return FALSE //continue world/Topic
|
||||
return FALSE //continue world/Topic
|
||||
|
||||
if(their_sCK != comms_key)
|
||||
return "Invalid comms key!";
|
||||
@@ -160,7 +160,7 @@
|
||||
var/datum/tgs_revision_information/test_merge/tm = new
|
||||
tm.number = text2num(I)
|
||||
var/list/entry = json[I]
|
||||
tm.pull_request_commit = entry["commit"]
|
||||
tm.head_commit = entry["commit"]
|
||||
tm.author = entry["author"]
|
||||
tm.title = entry["title"]
|
||||
. += tm
|
||||
@@ -176,7 +176,7 @@
|
||||
return ri
|
||||
|
||||
/datum/tgs_api/v3210/EndProcess()
|
||||
sleep(world.tick_lag) //flush the buffers
|
||||
sleep(world.tick_lag) //flush the buffers
|
||||
ExportService(SERVICE_REQUEST_KILL_PROCESS)
|
||||
|
||||
/datum/tgs_api/v3210/ChatChannelInfo()
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
var/list/json = cached_json["testMerges"]
|
||||
for(var/entry in json)
|
||||
var/datum/tgs_revision_information/test_merge/tm = new
|
||||
tm.time_merged = text2num(entry["timeMerged"])
|
||||
tm.timestamp = text2num(entry["timeMerged"])
|
||||
|
||||
var/list/revInfo = entry["revision"]
|
||||
if(revInfo)
|
||||
@@ -104,7 +104,7 @@
|
||||
tm.url = entry["url"]
|
||||
tm.author = entry["author"]
|
||||
tm.number = entry["number"]
|
||||
tm.pull_request_commit = entry["pullRequestRevision"]
|
||||
tm.head_commit = entry["pullRequestRevision"]
|
||||
tm.comment = entry["comment"]
|
||||
|
||||
cached_test_merges += tm
|
||||
@@ -118,7 +118,7 @@
|
||||
var/list/params = params2list(T)
|
||||
var/their_sCK = params[TGS4_INTEROP_ACCESS_IDENTIFIER]
|
||||
if(!their_sCK)
|
||||
return FALSE //continue world/Topic
|
||||
return FALSE //continue world/Topic
|
||||
|
||||
if(their_sCK != access_identifier)
|
||||
return "Invalid comms key!";
|
||||
@@ -192,7 +192,7 @@
|
||||
|
||||
//request a new port
|
||||
export_lock = FALSE
|
||||
var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose
|
||||
var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose
|
||||
|
||||
if(!new_port_json)
|
||||
TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]")
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
var/port = result[TGS4_PARAMETER_DATA]
|
||||
if(!isnum(port))
|
||||
return //this is valid, server may just want use to reboot
|
||||
return //this is valid, server may just want use to reboot
|
||||
|
||||
if(port == 0)
|
||||
//to byond 0 means any port and "none" means close vOv
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
#define DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES "chatResponses"
|
||||
|
||||
#define DMAPI5_REVISION_INFORMATION_COMMIT_SHA "commitSha"
|
||||
#define DMAPI5_REVISION_INFORMATION_TIMESTAMP "timestamp"
|
||||
#define DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA "originCommitSha"
|
||||
|
||||
#define DMAPI5_CHAT_USER_ID "id"
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
var/initialized = FALSE
|
||||
|
||||
/datum/tgs_api/v5/ApiVersion()
|
||||
return new /datum/tgs_version(TGS_DMAPI_VERSION)
|
||||
return new /datum/tgs_version(
|
||||
#include "interop_version.dm"
|
||||
)
|
||||
|
||||
/datum/tgs_api/v5/OnWorldNew(minimum_required_security_level)
|
||||
server_port = world.params[DMAPI5_PARAM_SERVER_PORT]
|
||||
@@ -48,6 +50,7 @@
|
||||
if(istype(revisionData))
|
||||
revision = new
|
||||
revision.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA]
|
||||
revision.timestamp = revisionData[DMAPI5_REVISION_INFORMATION_TIMESTAMP]
|
||||
revision.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA]
|
||||
else
|
||||
TGS_ERROR_LOG("Failed to decode [DMAPI5_RUNTIME_INFORMATION_REVISION] from runtime information!")
|
||||
@@ -63,15 +66,18 @@
|
||||
if(revInfo)
|
||||
tm.commit = revisionData[DMAPI5_REVISION_INFORMATION_COMMIT_SHA]
|
||||
tm.origin_commit = revisionData[DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA]
|
||||
tm.timestamp = entry[DMAPI5_REVISION_INFORMATION_TIMESTAMP]
|
||||
else
|
||||
TGS_WARNING_LOG("Failed to decode [DMAPI5_TEST_MERGE_REVISION] from test merge #[tm.number]!")
|
||||
|
||||
tm.time_merged = text2num(entry[DMAPI5_TEST_MERGE_TIME_MERGED])
|
||||
if(!tm.timestamp)
|
||||
tm.timestamp = entry[DMAPI5_TEST_MERGE_TIME_MERGED]
|
||||
|
||||
tm.title = entry[DMAPI5_TEST_MERGE_TITLE_AT_MERGE]
|
||||
tm.body = entry[DMAPI5_TEST_MERGE_BODY_AT_MERGE]
|
||||
tm.url = entry[DMAPI5_TEST_MERGE_URL]
|
||||
tm.author = entry[DMAPI5_TEST_MERGE_AUTHOR]
|
||||
tm.pull_request_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION]
|
||||
tm.head_commit = entry[DMAPI5_TEST_MERGE_PULL_REQUEST_REVISION]
|
||||
tm.comment = entry[DMAPI5_TEST_MERGE_COMMENT]
|
||||
|
||||
test_merges += tm
|
||||
@@ -98,18 +104,19 @@
|
||||
return json_encode(response)
|
||||
|
||||
/datum/tgs_api/v5/OnTopic(T)
|
||||
if(!initialized)
|
||||
return FALSE //continue world/Topic
|
||||
|
||||
var/list/params = params2list(T)
|
||||
var/json = params[DMAPI5_TOPIC_DATA]
|
||||
if(!json)
|
||||
return FALSE
|
||||
return FALSE // continue to /world/Topic
|
||||
|
||||
var/list/topic_parameters = json_decode(json)
|
||||
if(!topic_parameters)
|
||||
return TopicResponse("Invalid topic parameters json!");
|
||||
|
||||
if(!initialized)
|
||||
TGS_WARNING_LOG("Missed topic due to not being initialized: [T]")
|
||||
return TRUE // too early to handle, but it's still our responsibility
|
||||
|
||||
var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER]
|
||||
if(their_sCK != access_identifier)
|
||||
return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!");
|
||||
@@ -266,7 +273,7 @@
|
||||
|
||||
var/port = result[DMAPI5_BRIDGE_RESPONSE_NEW_PORT]
|
||||
if(!isnum(port))
|
||||
return //this is valid, server may just want use to reboot
|
||||
return //this is valid, server may just want use to reboot
|
||||
|
||||
if(port == 0)
|
||||
//to byond 0 means any port and "none" means close vOv
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"5.3.0"
|
||||
@@ -79,6 +79,7 @@
|
||||
#undef DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES
|
||||
|
||||
#undef DMAPI5_REVISION_INFORMATION_COMMIT_SHA
|
||||
#undef DMAPI5_REVISION_INFORMATION_TIMESTAMP
|
||||
#undef DMAPI5_REVISION_INFORMATION_ORIGIN_COMMIT_SHA
|
||||
|
||||
#undef DMAPI5_CHAT_USER_ID
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/*!
|
||||
* Copyright (c) 2021 Arm A. Hammer
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* tgui state: never_state
|
||||
*
|
||||
* Always closes the UI, no matter what. See the ui_state in religious_tool.dm to see an example
|
||||
*/
|
||||
|
||||
GLOBAL_DATUM_INIT(never_state, /datum/ui_state/never_state, new)
|
||||
|
||||
/datum/ui_state/never_state/can_use_topic(src_object, mob/user)
|
||||
return UI_CLOSE
|
||||
@@ -94,6 +94,8 @@
|
||||
window.send_message("ping")
|
||||
var/flush_queue = window.send_asset(get_asset_datum(
|
||||
/datum/asset/simple/namespaced/fontawesome))
|
||||
flush_queue |= window.send_asset(get_asset_datum(
|
||||
/datum/asset/simple/namespaced/tgfont))
|
||||
for(var/datum/asset/asset in src_object.ui_assets(user))
|
||||
flush_queue |= window.send_asset(asset)
|
||||
if (flush_queue)
|
||||
@@ -241,7 +243,7 @@
|
||||
* Run an update cycle for this UI. Called internally by SStgui
|
||||
* every second or so.
|
||||
*/
|
||||
/datum/tgui/process(force = FALSE)
|
||||
/datum/tgui/process(delta_time, force = FALSE)
|
||||
if(closing)
|
||||
return
|
||||
var/datum/host = src_object.ui_host(user)
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
get_asset_datum(/datum/asset/simple/tgui_panel),
|
||||
))
|
||||
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome))
|
||||
window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/tgfont))
|
||||
window.send_asset(get_asset_datum(/datum/asset/spritesheet/chat))
|
||||
// Other setup
|
||||
request_telemetry()
|
||||
addtimer(CALLBACK(src, .proc/on_initialize_timed_out), 5 SECONDS)
|
||||
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -173,6 +173,13 @@ export const sortBy = (...iterateeFns) => array => {
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -205,14 +212,17 @@ export const reduce = (reducerFn, initialValue) => array => {
|
||||
* is determined by the order they occur in the array. The iteratee is
|
||||
* invoked with one argument: value.
|
||||
*/
|
||||
export const uniqBy = iterateeFn => array => {
|
||||
/* 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 = array[index];
|
||||
let value: T | 0 = array[index];
|
||||
const computed = iterateeFn ? iterateeFn(value) : value;
|
||||
value = value !== 0 ? value : 0;
|
||||
if (computed === computed) {
|
||||
@@ -236,17 +246,20 @@ export const uniqBy = iterateeFn => array => {
|
||||
}
|
||||
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.
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export const zip = (...arrays) => {
|
||||
export const zip = <T extends unknown[][]>(...arrays: T): Zip<T> => {
|
||||
if (arrays.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export const round = (value, precision) => {
|
||||
m = Math.pow(10, precision);
|
||||
value *= m;
|
||||
// sign of the number
|
||||
sgn = (value > 0) | -(value < 0);
|
||||
sgn = +(value > 0) | -(value < 0);
|
||||
// isHalf = value % 1 === 0.5 * sgn;
|
||||
isHalf = Math.abs(value % 1) >= 0.4999999999854481;
|
||||
f = Math.floor(value);
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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 |
@@ -1,383 +0,0 @@
|
||||
/**
|
||||
* This file provides a clear separation layer between backend updates
|
||||
* and what state our React app sees.
|
||||
*
|
||||
* Sometimes backend can response without a "data" field, but our final
|
||||
* state will still contain previous "data" because we are merging
|
||||
* the response with already existing state.
|
||||
*
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { perf } from 'common/perf';
|
||||
import { setupDrag } from './drag';
|
||||
import { focusMap } from './focus';
|
||||
import { createLogger } from './logging';
|
||||
import { resumeRenderer, suspendRenderer } from './renderer';
|
||||
|
||||
const logger = createLogger('backend');
|
||||
|
||||
export const backendUpdate = state => ({
|
||||
type: 'backend/update',
|
||||
payload: state,
|
||||
});
|
||||
|
||||
export const backendSetSharedState = (key, nextState) => ({
|
||||
type: 'backend/setSharedState',
|
||||
payload: { key, nextState },
|
||||
});
|
||||
|
||||
export const backendSuspendStart = () => ({
|
||||
type: 'backend/suspendStart',
|
||||
});
|
||||
|
||||
export const backendSuspendSuccess = () => ({
|
||||
type: 'backend/suspendSuccess',
|
||||
payload: {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
config: {},
|
||||
data: {},
|
||||
shared: {},
|
||||
// Start as suspended
|
||||
suspended: Date.now(),
|
||||
suspending: false,
|
||||
};
|
||||
|
||||
export const backendReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === 'backend/update') {
|
||||
// Merge config
|
||||
const config = {
|
||||
...state.config,
|
||||
...payload.config,
|
||||
};
|
||||
// Merge data
|
||||
const data = {
|
||||
...state.data,
|
||||
...payload.static_data,
|
||||
...payload.data,
|
||||
};
|
||||
// Merge shared states
|
||||
const shared = { ...state.shared };
|
||||
if (payload.shared) {
|
||||
for (let key of Object.keys(payload.shared)) {
|
||||
const value = payload.shared[key];
|
||||
if (value === '') {
|
||||
shared[key] = undefined;
|
||||
}
|
||||
else {
|
||||
shared[key] = JSON.parse(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return new state
|
||||
return {
|
||||
...state,
|
||||
config,
|
||||
data,
|
||||
shared,
|
||||
suspended: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/setSharedState') {
|
||||
const { key, nextState } = payload;
|
||||
return {
|
||||
...state,
|
||||
shared: {
|
||||
...state.shared,
|
||||
[key]: nextState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendStart') {
|
||||
return {
|
||||
...state,
|
||||
suspending: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendSuccess') {
|
||||
const { timestamp } = payload;
|
||||
return {
|
||||
...state,
|
||||
data: {},
|
||||
shared: {},
|
||||
config: {
|
||||
...state.config,
|
||||
title: '',
|
||||
status: 1,
|
||||
},
|
||||
suspending: false,
|
||||
suspended: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export const backendMiddleware = store => {
|
||||
let fancyState;
|
||||
let suspendInterval;
|
||||
|
||||
return next => action => {
|
||||
const { suspended } = selectBackend(store.getState());
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === 'update') {
|
||||
store.dispatch(backendUpdate(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'suspend') {
|
||||
store.dispatch(backendSuspendSuccess());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'ping') {
|
||||
sendMessage({
|
||||
type: 'pingReply',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendStart' && !suspendInterval) {
|
||||
logger.log(`suspending (${window.__windowId__})`);
|
||||
// Keep sending suspend messages until it succeeds.
|
||||
// It may fail multiple times due to topic rate limiting.
|
||||
const suspendFn = () => sendMessage({
|
||||
type: 'suspend',
|
||||
});
|
||||
suspendFn();
|
||||
suspendInterval = setInterval(suspendFn, 2000);
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendSuccess') {
|
||||
suspendRenderer();
|
||||
clearInterval(suspendInterval);
|
||||
suspendInterval = undefined;
|
||||
Byond.winset(window.__windowId__, {
|
||||
'is-visible': false,
|
||||
});
|
||||
setImmediate(() => focusMap());
|
||||
}
|
||||
|
||||
if (type === 'backend/update') {
|
||||
const fancy = payload.config?.window?.fancy;
|
||||
// Initialize fancy state
|
||||
if (fancyState === undefined) {
|
||||
fancyState = fancy;
|
||||
}
|
||||
// React to changes in fancy
|
||||
else if (fancyState !== fancy) {
|
||||
logger.log('changing fancy mode to', fancy);
|
||||
fancyState = fancy;
|
||||
Byond.winset(window.__windowId__, {
|
||||
titlebar: !fancy,
|
||||
'can-resize': !fancy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Resume on incoming update
|
||||
if (type === 'backend/update' && suspended) {
|
||||
// Show the payload
|
||||
logger.log('backend/update', payload);
|
||||
// Signal renderer that we have resumed
|
||||
resumeRenderer();
|
||||
// Setup drag
|
||||
setupDrag();
|
||||
// We schedule this for the next tick here because resizing and unhiding
|
||||
// during the same tick will flash with a white background.
|
||||
setImmediate(() => {
|
||||
perf.mark('resume/start');
|
||||
// Doublecheck if we are not re-suspended.
|
||||
const { suspended } = selectBackend(store.getState());
|
||||
if (suspended) {
|
||||
return;
|
||||
}
|
||||
Byond.winset(window.__windowId__, {
|
||||
'is-visible': true,
|
||||
});
|
||||
perf.mark('resume/finish');
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.log('visible in',
|
||||
perf.measure('render/finish', 'resume/finish'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a message to /datum/tgui_window.
|
||||
*/
|
||||
export const sendMessage = (message = {}) => {
|
||||
const { payload, ...rest } = message;
|
||||
const data = {
|
||||
// Message identifying header
|
||||
tgui: 1,
|
||||
window_id: window.__windowId__,
|
||||
// Message body
|
||||
...rest,
|
||||
};
|
||||
// JSON-encode the payload
|
||||
if (payload !== null && payload !== undefined) {
|
||||
data.payload = JSON.stringify(payload);
|
||||
}
|
||||
Byond.topic(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends an action to `ui_act` on `src_object` that this tgui window
|
||||
* is associated with.
|
||||
*/
|
||||
export const sendAct = (action, payload = {}) => {
|
||||
// Validate that payload is an object
|
||||
const isObject = typeof payload === 'object'
|
||||
&& payload !== null
|
||||
&& !Array.isArray(payload);
|
||||
if (!isObject) {
|
||||
logger.error(`Payload for act() must be an object, got this:`, payload);
|
||||
return;
|
||||
}
|
||||
sendMessage({
|
||||
type: 'act/' + action,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef BackendState
|
||||
* @type {{
|
||||
* config: {
|
||||
* title: string,
|
||||
* status: number,
|
||||
* interface: string,
|
||||
* window: {
|
||||
* key: string,
|
||||
* size: [number, number],
|
||||
* fancy: boolean,
|
||||
* locked: boolean,
|
||||
* },
|
||||
* client: {
|
||||
* ckey: string,
|
||||
* address: string,
|
||||
* computer_id: string,
|
||||
* },
|
||||
* user: {
|
||||
* name: string,
|
||||
* observer: number,
|
||||
* },
|
||||
* },
|
||||
* data: any,
|
||||
* shared: any,
|
||||
* suspending: boolean,
|
||||
* suspended: boolean,
|
||||
* }}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Selects a backend-related slice of Redux state
|
||||
*
|
||||
* @return {BackendState}
|
||||
*/
|
||||
export const selectBackend = state => state.backend || {};
|
||||
|
||||
/**
|
||||
* A React hook (sort of) for getting tgui state and related functions.
|
||||
*
|
||||
* This is supposed to be replaced with a real React Hook, which can only
|
||||
* be used in functional components.
|
||||
*
|
||||
* @return {BackendState & {
|
||||
* act: sendAct,
|
||||
* }}
|
||||
*/
|
||||
export const useBackend = context => {
|
||||
const { store } = context;
|
||||
const state = selectBackend(store.getState());
|
||||
return {
|
||||
...state,
|
||||
act: sendAct,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocates state on Redux store without sharing it with other clients.
|
||||
*
|
||||
* Use it when you want to have a stateful variable in your component
|
||||
* that persists between renders, but will be forgotten after you close
|
||||
* the UI.
|
||||
*
|
||||
* It is a lot more performant than `setSharedState`.
|
||||
*
|
||||
* @param {any} context React context.
|
||||
* @param {string} key Key which uniquely identifies this state in Redux store.
|
||||
* @param {any} initialState Initializes your global variable with this value.
|
||||
*/
|
||||
export const useLocalState = (context, key, initialState) => {
|
||||
const { store } = context;
|
||||
const state = selectBackend(store.getState());
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = (key in sharedStates)
|
||||
? sharedStates[key]
|
||||
: initialState;
|
||||
return [
|
||||
sharedState,
|
||||
nextState => {
|
||||
store.dispatch(backendSetSharedState(key, (
|
||||
typeof nextState === 'function'
|
||||
? nextState(sharedState)
|
||||
: nextState
|
||||
)));
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocates state on Redux store, and **shares** it with other clients
|
||||
* in the game.
|
||||
*
|
||||
* Use it when you want to have a stateful variable in your component
|
||||
* that persists not only between renders, but also gets pushed to other
|
||||
* clients that observe this UI.
|
||||
*
|
||||
* This makes creation of observable s
|
||||
*
|
||||
* @param {any} context React context.
|
||||
* @param {string} key Key which uniquely identifies this state in Redux store.
|
||||
* @param {any} initialState Initializes your global variable with this value.
|
||||
*/
|
||||
export const useSharedState = (context, key, initialState) => {
|
||||
const { store } = context;
|
||||
const state = selectBackend(store.getState());
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = (key in sharedStates)
|
||||
? sharedStates[key]
|
||||
: initialState;
|
||||
return [
|
||||
sharedState,
|
||||
nextState => {
|
||||
sendMessage({
|
||||
type: 'setSharedState',
|
||||
key,
|
||||
value: JSON.stringify(
|
||||
typeof nextState === 'function'
|
||||
? nextState(sharedState)
|
||||
: nextState
|
||||
) || '',
|
||||
});
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,34 +4,37 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { KEY_CTRL, KEY_ENTER, KEY_ESCAPE, KEY_F, KEY_F5, KEY_R, KEY_SHIFT, KEY_SPACE, KEY_TAB } from 'common/keycodes';
|
||||
import { globalEvents } from './events';
|
||||
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 = {};
|
||||
const byondMacros: Record<string, string> = {};
|
||||
|
||||
// Array of acquired keys, which will not be sent to BYOND.
|
||||
// Default set of acquired keys, which will not be sent to BYOND.
|
||||
const hotKeysAcquired = [
|
||||
// Default set of acquired keys
|
||||
KEY_ESCAPE,
|
||||
KEY_ENTER,
|
||||
KEY_SPACE,
|
||||
KEY_TAB,
|
||||
KEY_CTRL,
|
||||
KEY_SHIFT,
|
||||
KEY_F5,
|
||||
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 = {};
|
||||
const keyState: Record<string, boolean> = {};
|
||||
|
||||
/**
|
||||
* Converts a browser keycode to BYOND keycode.
|
||||
*/
|
||||
const keyCodeToByond = keyCode => {
|
||||
const keyCodeToByond = (keyCode: number) => {
|
||||
if (keyCode === 16) return 'Shift';
|
||||
if (keyCode === 17) return 'Ctrl';
|
||||
if (keyCode === 18) return 'Alt';
|
||||
@@ -63,14 +66,15 @@ const keyCodeToByond = keyCode => {
|
||||
* Keyboard passthrough logic. This allows you to keep doing things
|
||||
* in game while the browser window is focused.
|
||||
*/
|
||||
const handlePassthrough = key => {
|
||||
const handlePassthrough = (key: KeyEvent) => {
|
||||
const keyString = String(key);
|
||||
// In addition to F5, support reloading with Ctrl+R and Ctrl+F5
|
||||
if (key.ctrl && (key.code === KEY_F5 || key.code === KEY_R)) {
|
||||
if (keyString === 'Ctrl+F5' || keyString === 'Ctrl+R') {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
// Prevent passthrough on Ctrl+F
|
||||
if (key.ctrl && key.code === KEY_F) {
|
||||
if (keyString === 'Ctrl+F') {
|
||||
return;
|
||||
}
|
||||
// NOTE: Alt modifier is pretty bad and sticky in IE11.
|
||||
@@ -109,14 +113,14 @@ const handlePassthrough = key => {
|
||||
* Acquires a lock on the hotkey, which prevents it from being
|
||||
* passed through to BYOND.
|
||||
*/
|
||||
export const acquireHotKey = keyCode => {
|
||||
export const acquireHotKey = (keyCode: number) => {
|
||||
hotKeysAcquired.push(keyCode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the hotkey available to BYOND again.
|
||||
*/
|
||||
export const releaseHotKey = keyCode => {
|
||||
export const releaseHotKey = (keyCode: number) => {
|
||||
const index = hotKeysAcquired.indexOf(keyCode);
|
||||
if (index >= 0) {
|
||||
hotKeysAcquired.splice(index, 1);
|
||||
@@ -133,25 +137,33 @@ export const releaseHeldKeys = () => {
|
||||
}
|
||||
};
|
||||
|
||||
type ByondSkinMacro = {
|
||||
command: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const setupHotKeys = () => {
|
||||
// Read macros
|
||||
Byond.winget('default.*').then(data => {
|
||||
Byond.winget('default.*').then((data: Record<string, string>) => {
|
||||
// Group each macro by ref
|
||||
const groupedByRef = {};
|
||||
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] = {};
|
||||
groupedByRef[ref] = {} as any;
|
||||
}
|
||||
groupedByRef[ref][prop] = data[key];
|
||||
}
|
||||
}
|
||||
// Insert macros
|
||||
const escapedQuotRegex = /\\"/g;
|
||||
const unescape = str => str
|
||||
const unescape = (str: string) => str
|
||||
.substring(1, str.length - 1)
|
||||
.replace(escapedQuotRegex, '"');
|
||||
for (let ref of Object.keys(groupedByRef)) {
|
||||
@@ -165,7 +177,7 @@ export const setupHotKeys = () => {
|
||||
globalEvents.on('window-blur', () => {
|
||||
releaseHeldKeys();
|
||||
});
|
||||
globalEvents.on('key', key => {
|
||||
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,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,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,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>
|
||||
);
|
||||
};
|
||||