code:
import { multiline } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Dimmer, Divider, Icon, 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}
          />

        </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>

        {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>

    </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, semi_random_bonus, full_random_bonus } = 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.
        Guaranteed to have {semi_random_bonus} points worth of spells.
      </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!
        Guaranteed to have {full_random_bonus} points worth of spells.
      </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-start"
                                          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,
                                      })} />

                                    {!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>

                            </Stack.Item>
                          ))}
                        </Stack>
                      </Stack.Item>
                    )}
                  </Stack>

              </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-start"
                                          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,
                                      })} />

                                    {!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>

                            </Stack.Item>
                          ))}
                        </Stack>
                      </Stack.Item>
                    )}
                  </Stack>

              </Stack.Item>
            </Stack>
          </Stack.Item>
          <Stack.Item>

              <ProgressBar
                value={points/10}>
                {points + ' points left to spend.'}
              </ProgressBar>

          </Stack.Item>
        </Stack>
      </Window.Content>
    </Window>
  );
};


code:
import { createSearch } from 'common/string';
import { sortBy } from 'common/collections';
import { useBackend, useLocalState } from "../backend";
import { Box, Button, Input, NoticeBox, Section, Collapsible, Table } from "../components";
import { Window } from "../layouts";

export const Stack = (props, context) => {
  const { act, data } = useBackend(context);

  const {
    amount,
    recipes = [],
  } = data;

  const [
    searchText,
    setSearchText,
  ] = useLocalState(context, 'searchText', '');

  const testSearch = createSearch(searchText, item => {
    return item;
  });

  const items = searchText.length > 0
   && Object.keys(recipes)
     .filter(testSearch)
     .reduce((obj, key) => {
       obj[key] = recipes[key];
       return obj;
     }, {})
    || recipes;

  const height = Math.max(94 + Object.keys(recipes).length * 26, 250);

  return (
    <Window
      width={400}
      height={Math.min(height, 500)}>
      <Window.Content scrollable>
        <Section
          title={"Amount: " + amount}
          buttons={(
            <>
              Search
              <Input
                autoFocus
                value={searchText}
                onInput={(e, value) => setSearchText(value)}
                mx={1} />
            </>
          )}>
          {items.length === 0 && (
            <NoticeBox>
              No recipes found.
            </NoticeBox>
          ) || (
            <RecipeList recipes={items} />
          )}
        </Section>
      </Window.Content>
    </Window>
  );
};

const RecipeList = (props, context) => {
  const { act, data } = useBackend(context);

  const {
    recipes,
  } = props;

  const sortedKeys = sortBy(key => key.toLowerCase())(Object.keys(recipes));

  return sortedKeys.map(title => {
    const recipe = recipes[title];
    if (recipe.ref === undefined) {
      return (
        <Collapsible
          ml={1}
          color="label"
          title={title}>
          <Box ml={1}>
            <RecipeList recipes={recipe} />
          </Box>
        </Collapsible>
      );
    } else {
      return (
        <Recipe
          title={title}
          recipe={recipe} />
      );
    }
  });
};

const buildMultiplier = (recipe, amount) => {
  if (recipe.req_amount > amount) {
    return 0;
  }

  return Math.floor(amount / recipe.req_amount);
};

const Multipliers = (props, context) => {
  const { act, data } = useBackend(context);

  const {
    recipe,
    maxMultiplier,
  } = props;

  const maxM = Math.min(maxMultiplier,
    Math.floor(recipe.max_res_amount / recipe.res_amount));

  const multipliers = [5, 10, 25];

  let finalResult = [];

  for (const multiplier of multipliers) {
    if (maxM >= multiplier) {
      finalResult.push((
        <Button
          content={multiplier * recipe.res_amount + "x"}
          onClick={() => act("make", {
            ref: recipe.ref,
            multiplier: multiplier,
          })} />
      ));
    }
  }

  if (multipliers.indexOf(maxM) === -1) {
    finalResult.push((
      <Button
        content={maxM * recipe.res_amount + "x"}
        onClick={() => act("make", {
          ref: recipe.ref,
          multiplier: maxM,
        })} />
    ));
  }

  return finalResult;
};

const Recipe = (props, context) => {
  const { act, data } = useBackend(context);

  const {
    amount,
  } = data;

  const {
    recipe,
    title,
  } = props;

  const {
    res_amount,
    max_res_amount,
    req_amount,
    ref,
  } = recipe;

  let buttonName = title;
  buttonName += " (";
  buttonName += req_amount + " ";
  buttonName += ("sheet" + (req_amount > 1 ? "s" : ""));
  buttonName += ")";

  if (res_amount > 1) {
    buttonName = res_amount + "x " + buttonName;
  }

  const maxMultiplier = buildMultiplier(recipe, amount);

  return (
    <Box mb={1}>
      <Table>
        <Table.Row>
          <Table.Cell>
            <Button
              fluid
              disabled={!maxMultiplier}
              icon="wrench"
              content={buttonName}
              onClick={() => act("make", {
                ref: recipe.ref,
                multiplier: 1,
              })} />
          </Table.Cell>
          {max_res_amount > 1 && maxMultiplier > 1 && (
            <Table.Cell collapsing>
              <Multipliers
                recipe={recipe}
                maxMultiplier={maxMultiplier} />
            </Table.Cell>
          )}
        </Table.Row>
      </Table>
    </Box>
  );
};

repo_name: AnalWerewolf/sundown

license: mit


code:
import { useBackend } from '../backend';
import { Button, LabeledList, NoticeBox, Section } from '../components';
import { Window } from '../layouts';

export const StackingConsole = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    machine,
  } = data;
  return (
    <Window
      width={320}
      height={340}>
      <Window.Content scrollable>
        {!machine ? (
          <NoticeBox>
            No connected stacking machine
          </NoticeBox>
        ) : (
          <StackingConsoleContent />
        )}
      </Window.Content>
    </Window>
  );
};

export const StackingConsoleContent = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    stacking_amount,
    contents = [],
  } = data;
  return (
    <>

        <LabeledList>
          <LabeledList.Item label="Stacking Amount">
            {stacking_amount || "Unknown"}
          </LabeledList.Item>
        </LabeledList>

        {!contents.length ? (
          <NoticeBox>
            No stored materials
          </NoticeBox>
        ) : (
          <LabeledList>
            {contents.map(sheet => (
              <LabeledList.Item
                key={sheet.type}
                label={sheet.name}
                buttons={(
                  <Button
                    icon="eject"
                    content="Release"
                    onClick={() => act('release', {
                      type: sheet.type,
                    })} />
                )}>
                {sheet.amount || "Unknown"}
              </LabeledList.Item>
            ))}
          </LabeledList>
        )}

    </>
  );
};


code:
import { sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { useBackend } from '../backend';
import { Button, Section, Stack } from '../components';
import { Window } from '../layouts';

export const StationAlertConsole = (props, context) => {
  const { data } = useBackend(context);
  const {
    cameraView,
  } = data;
  return (
    <Window
      width={cameraView ? 390 : 345}
      height={587}>
      <Window.Content scrollable>
        <StationAlertConsoleContent />
      </Window.Content>
    </Window>
  );
};

export const StationAlertConsoleContent = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    cameraView,
  } = data;

  const sortingKey = {
    "Fire": 0,
    "Atmosphere": 1,
    "Power": 2,
    "Burglar": 3,
    "Motion": 4,
    "Camera": 5,
  };

  const sortedAlarms = flow([
    sortBy((alarm) => sortingKey[alarm.name]),
  ])(data.alarms || []);

  return (
    <>
      {sortedAlarms.map(category => (

            {category.alerts.length === 0 && (

                Systems Nominal

            )}
            {category.alerts.map(alert => (
              <Stack
                key={alert.name}
                height="30px"
                align="baseline">
                <Stack.Item grow>

                    {alert.name} {!!cameraView && alert.sources > 1
                      ? " (" + alert.sources + " sources)" : ""}

                </Stack.Item>
                {!!cameraView && (
                  <Stack.Item>
                    <Button
                      textAlign="center"
                      width="100px"
                      icon={alert.cameras ? "video" : ""}
                      disabled={!alert.cameras}
                      content={alert.cameras === 1
                        ? alert.cameras + " Camera" : alert.cameras > 1
                          ? alert.cameras + " Cameras" : "No Camera"}
                      onClick={() => act('select_camera', {
                        alert: alert.ref,
                      })} />
                  </Stack.Item>
                )}
              </Stack>
            ))}

      ))}
    </>
  );
};


code:
import { filterMap } from "common/collections";
import { exhaustiveCheck } from "common/exhaustive";
import { BooleanLike } from "common/react";
import { useBackend, useLocalState } from "../backend";
import { Box, Button, Divider, Dropdown, Stack, Tabs } from "../components";
import { Window } from "../layouts";

type CurrentStationTrait = {
  can_revert: BooleanLike,
  name: string,
  ref: string,
}

type ValidStationTrait = {
  name: string,
  path: string,
}

type StationTraitsData = {
  current_traits: CurrentStationTrait[],
  future_station_traits?: ValidStationTrait[],
  too_late_to_revert: BooleanLike,
  valid_station_traits: ValidStationTrait[],
};

enum Tab {
  SetupFutureStationTraits,
  ViewStationTraits,
}

const FutureStationTraitsPage = (props, context) => {
  const { act, data } = useBackend<StationTraitsData>(context);
  const { future_station_traits } = data;

  const [selectedTrait, setSelectedTrait] = useLocalState<string | null>(
    context,
    "selectedFutureTrait",
    null,
  );

  const traitsByName = Object.fromEntries(
    data.valid_station_traits.map(trait => {
      return [trait.name, trait.path];
    })
  );

  const traitNames = Object.keys(traitsByName);
  traitNames.sort();

  return (
    <Box>
      <Stack fill>
        <Stack.Item grow>
          <Dropdown
            displayText={!selectedTrait && "Select trait to add..."}
            onSelected={setSelectedTrait}
            options={traitNames}
            selected={selectedTrait}
            width="100%"
          />
        </Stack.Item>

        <Stack.Item>
          <Button
            color="green"
            icon="plus"
            onClick={() => {
              if (!selectedTrait) {
                return;
              }

              const selectedPath = traitsByName[selectedTrait];

              let newStationTraits = [selectedPath];
              if (future_station_traits) {
                const selectedTraitPaths
                  = future_station_traits.map(trait => trait.path);

                if (selectedTraitPaths.indexOf(selectedPath) !== -1) {
                  return;
                }

                newStationTraits
                  = newStationTraits.concat(...selectedTraitPaths);
              }

              act("setup_future_traits", {
                station_traits: newStationTraits,
              });
            }}
          >
            Add
          </Button>
        </Stack.Item>
      </Stack>

      <Divider />

      {
        Array.isArray(future_station_traits)
          ? (
            future_station_traits.length > 0
              ? (
                <Stack vertical fill>
                  {future_station_traits.map(trait => (
                    <Stack.Item key={trait.path}>
                      <Stack fill>
                        <Stack.Item grow>
                          {trait.name}
                        </Stack.Item>

                        <Stack.Item>
                          <Button
                            color="red"
                            icon="times"
                            onClick={() => {
                              act("setup_future_traits", {
                                station_traits:
                                filterMap(future_station_traits, otherTrait => {
                                  if (otherTrait.path === trait.path) {
                                    return undefined;
                                  } else {
                                    return otherTrait.path;
                                  }
                                }),
                              });
                            }}
                          >
                            Delete
                          </Button>
                        </Stack.Item>
                      </Stack>
                    </Stack.Item>
                  ))}
                </Stack>
              ) : (
                <>
                  <Box>
                    No station traits will run next round.
                  </Box>

                  <Box>
                    <Button
                      color="red"
                      icon="times"
                      tooltip="The next round will roll station traits randomly, just like normal"
                      onClick={() => act("clear_future_traits")}
                    >
                      Run Station Traits Normally
                    </Button>
                  </Box>
                </>
              )
          )
          : (
            <>
              <Box>
                No future station traits are planned.
              </Box>

              <Box>
                <Button
                  color="red"
                  icon="times"
                  onClick={() => act("setup_future_traits", {
                    station_traits: [],
                  })}
                >
                  Prevent station traits from running next round
                </Button>
              </Box>
            </>
          )
      }
    </Box>
  );
};

const ViewStationTraitsPage = (props, context) => {
  const { act, data } = useBackend<StationTraitsData>(context);

  return data.current_traits.length > 0 ? (
    <Stack vertical fill>
      {data.current_traits.map(stationTrait => (
        <Stack.Item key={stationTrait.ref}>
          <Stack fill>
            <Stack.Item grow>
              {stationTrait.name}
            </Stack.Item>

            <Stack.Item>
              <Button.Confirm
                content="Revert"
                color="red"
                disabled={data.too_late_to_revert || !stationTrait.can_revert}
                tooltip={
                  (!stationTrait.can_revert && "This trait is not revertable.")
                  || (data.too_late_to_revert && "It's too late to revert station traits, the round has already started.")
                }
                icon="times"
                onClick={() => act("revert", {
                  ref: stationTrait.ref,
                })}
              />
            </Stack.Item>
          </Stack>
        </Stack.Item>
      ))}
    </Stack>
  ) : (
    <Box>
      There are no active station traits.
    </Box>
  );
};

export const StationTraitsPanel = (props, context) => {
  const [currentTab, setCurrentTab] = useLocalState(
    context,
    "station_traits_tab",
    Tab.ViewStationTraits,
  );

  let currentPage;

  switch (currentTab) {
    case Tab.SetupFutureStationTraits:
      currentPage = <FutureStationTraitsPage />;
      break;
    case Tab.ViewStationTraits:
      currentPage = <ViewStationTraitsPage />;
      break;
    default:
      exhaustiveCheck(currentTab);
  }

  return (
    <Window
      title="Modify Station Traits"
      height={500}
      width={500}
    >
      <Window.Content scrollable>
        <Tabs>
          <Tabs.Tab
            icon="eye"
            selected={currentTab === Tab.ViewStationTraits}
            onClick={() => setCurrentTab(Tab.ViewStationTraits)}
          >
            View
          </Tabs.Tab>

          <Tabs.Tab
            icon="edit"
            selected={currentTab === Tab.SetupFutureStationTraits}
            onClick={() => setCurrentTab(Tab.SetupFutureStationTraits)}
          >
            Edit
          </Tabs.Tab>
        </Tabs>

        <Divider />

        {currentPage}
      </Window.Content>
    </Window>
  );
};


code:
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: "left ear",
    gridSpot: getGridSpotKey([1, 3]),
    image: "inventory-ears.png",
  },

  ears_extra: {
    displayName: "right ear",
    gridSpot: getGridSpotKey([0, 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: "back 2",
    gridSpot: getGridSpotKey([4, 0]),
    image: "inventory-back2.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: "back",
    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) {
                    if (item.alternate) {
                      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"
                              className="centered-image"
                              src={resolveAsset(slot.image)}
                              opacity={0.7}
                            />
                          )}

                          <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>
  );
};


code:
import { sortBy } from 'common/collections';
import { flow } from 'common/fp';
import { toFixed } from 'common/math';
import { useBackend } from '../backend';
import { Button, LabeledList, ProgressBar, Section, Stack, Table } from '../components';
import { getGasColor, getGasLabel } from '../constants';
import { Window } from '../layouts';

const logScale = (value) => Math.log2(16 + Math.max(0, value)) - 4;

export const SupermatterMonitor = () => {
  return (
    <Window width={600} height={350} theme="ntos" title="Supermatter Monitor">
      <Window.Content scrollable>
        <SupermatterMonitorContent />
      </Window.Content>
    </Window>
  );
};

export const SupermatterMonitorContent = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    active,
    singlecrystal,
    SM_uid,
    SM_area_name,
    SM_integrity,
    SM_power,
    SM_ambienttemp,
    SM_ambientpressure,
    SM_moles,
    SM_bad_moles_amount,
  } = data;
  if (!active) {
    return <SupermatterList />;
  }
  const gases = flow([
    (gases) => gases.filter((gas) => gas.amount >= 0.01),
    sortBy((gas) => -gas.amount),
  ])(data.gases || []);
  const gasMaxAmount = Math.max(1, ...gases.map((gas) => gas.amount));
  return (
    <Section
      title={SM_uid + '. ' + SM_area_name}
      buttons={!singlecrystal && (
        <Button
          icon="arrow-left"
          content="Back"
          onClick={() => act('PRG_clear')}
        />
      )}>
      <Stack>
        <Stack.Item width="270px">

            <LabeledList>
              <LabeledList.Item label="Integrity">
                <ProgressBar
                  value={SM_integrity / 100}
                  ranges={{
                    good: [0.9, Infinity],
                    average: [0.5, 0.9],
                    bad: [-Infinity, 0.5],
                  }}
                />
              </LabeledList.Item>
              <LabeledList.Item label="Relative EER">
                <ProgressBar
                  value={SM_power}
                  minValue={0}
                  maxValue={5000}
                  ranges={{
                    good: [-Infinity, 5000],
                    average: [5000, 7000],
                    bad: [7000, Infinity],
                  }}>
                  {toFixed(SM_power) + ' MeV/cm3'}
                </ProgressBar>
              </LabeledList.Item>
              <LabeledList.Item label="Temperature">
                <ProgressBar
                  value={logScale(SM_ambienttemp)}
                  minValue={0}
                  maxValue={logScale(10000)}
                  ranges={{
                    teal: [-Infinity, logScale(80)],
                    good: [logScale(80), logScale(373)],
                    average: [logScale(373), logScale(1000)],
                    bad: [logScale(1000), Infinity],
                  }}>
                  {toFixed(SM_ambienttemp) + ' K'}
                </ProgressBar>
              </LabeledList.Item>
              <LabeledList.Item label="Total Moles">
                <ProgressBar
                  value={logScale(SM_moles)}
                  minValue={0}
                  maxValue={logScale(50000)}
                  ranges={{
                    good: [-Infinity, logScale(SM_bad_moles_amount * 0.75)],
                    average: [
                      logScale(SM_bad_moles_amount * 0.75),
                      logScale(SM_bad_moles_amount),
                    ],
                    bad: [logScale(SM_bad_moles_amount), Infinity],
                  }}>
                  {toFixed(SM_moles) + ' moles'}
                </ProgressBar>
              </LabeledList.Item>
              <LabeledList.Item label="Pressure">
                <ProgressBar
                  value={logScale(SM_ambientpressure)}
                  minValue={0}
                  maxValue={logScale(50000)}
                  ranges={{
                    good: [logScale(1), logScale(300)],
                    average: [-Infinity, logScale(1000)],
                    bad: [logScale(1000), +Infinity],
                  }}>
                  {toFixed(SM_ambientpressure) + ' kPa'}
                </ProgressBar>
              </LabeledList.Item>
            </LabeledList>

        </Stack.Item>
        <Stack.Item grow={1} basis={0}>

            <LabeledList>
              {gases.map((gas) => (
                <LabeledList.Item key={gas.name} label={getGasLabel(gas.name)}>
                  <ProgressBar
                    color={getGasColor(gas.name)}
                    value={gas.amount}
                    minValue={0}
                    maxValue={gasMaxAmount}>
                    {toFixed(gas.amount, 2) + '%'}
                  </ProgressBar>
                </LabeledList.Item>
              ))}
            </LabeledList>

        </Stack.Item>
      </Stack>

  );
};

const SupermatterList = (props, context) => {
  const { act, data } = useBackend(context);
  const { supermatters = [] } = data;
  return (
    <Section
      title="Detected Supermatters"
      buttons={
        <Button
          icon="sync"
          content="Refresh"
          onClick={() => act('PRG_refresh')}
        />
      }>

        {supermatters.map((sm) => (

{sm.uid + '. ' + sm.area_name}

              Integrity:

              <ProgressBar
                value={sm.integrity / 100}
                ranges={{
                  good: [0.9, Infinity],
                  average: [0.5, 0.9],
                  bad: [-Infinity, 0.5],
                }}
              />

              <Button
                content="Details"
                onClick={() =>
                  act('PRG_set', {
                    target: sm.uid,
                  })}
              />

        ))}

  );
};


code:
import { Component, Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Flex, Grid, Icon, LabeledList, Modal, NoticeBox, Section, Table, Tabs } from '../components';
import { NtosWindow } from '../layouts';

const CONTRACT_STATUS_INACTIVE = 1;
const CONTRACT_STATUS_ACTIVE = 2;
const CONTRACT_STATUS_BOUNTY_CONSOLE_ACTIVE = 3;
const CONTRACT_STATUS_EXTRACTING = 4;
const CONTRACT_STATUS_COMPLETE = 5;
const CONTRACT_STATUS_ABORTED = 6;

export class FakeTerminal extends Component {
  constructor(props) {
    super(props);
    this.timer = null;
    this.state = {
      currentIndex: 0,
      currentDisplay: [],
    };
  }

  tick() {
    const { props, state } = this;
    if (state.currentIndex <= props.allMessages.length) {
      this.setState(prevState => {
        return ({
          currentIndex: prevState.currentIndex + 1,
        });
      });
      const { currentDisplay } = state;
      currentDisplay.push(props.allMessages[state.currentIndex]);
    } else {
      clearTimeout(this.timer);
      setTimeout(props.onFinished, props.finishedTimeout);
    }
  }

  componentDidMount() {
    const {
      linesPerSecond = 2.5,
    } = this.props;
    this.timer = setInterval(() => this.tick(), 1000 / linesPerSecond);
  }

  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  render() {
    return (
      <Box m={1}>
        {this.state.currentDisplay.map(value => (
          <Fragment key={value}>
            {value}

          </Fragment>
        ))}
      </Box>
    );
  }
}

export const SyndContractor = (props, context) => {
  return (
    <NtosWindow
      width={500}
      height={600}
      theme="syndicate">
      <NtosWindow.Content scrollable>
        <SyndContractorContent />
      </NtosWindow.Content>
    </NtosWindow>
  );
};

export const SyndContractorContent = (props, context) => {
  const { data, act } = useBackend(context);

  const terminalMessages = [
    "Recording biometric data...",
    "Analyzing embedded syndicate info...",
    "STATUS CONFIRMED",
    "Contacting syndicate database...",
    "Awaiting response...",
    "Awaiting response...",
    "Awaiting response...",
    "Awaiting response...",
    "Awaiting response...",
    "Awaiting response...",
    "Response received, ack 4851234...",
    "CONFIRM ACC " + (Math.round(Math.random() * 20000)),
    "Setting up private accounts...",
    "CONTRACTOR ACCOUNT CREATED",
    "Searching for available contracts...",
    "Searching for available contracts...",
    "Searching for available contracts...",
    "Searching for available contracts...",
    "CONTRACTS FOUND",
    "WELCOME, AGENT",
  ];

  const infoEntries = [
    "SyndTract v2.0",
    "",
    "We've identified potentional high-value targets that are",
    "currently assigned to your mission area. They are believed",
    "to hold valuable information which could be of immediate",
    "importance to our organisation.",
    "",
    "Listed below are all of the contracts available to you. You",
    "are to bring the specified target to the designated",
    "drop-off, and contact us via this uplink. We will send",
    "a specialised extraction unit to put the body into.",
    "",
    "We want targets alive - but we will sometimes pay slight",
    "amounts if they're not, you just won't recieve the shown",
    "bonus. You can redeem your payment through this uplink in",
    "the form of raw telecrystals, which can be put into your",
    "regular Syndicate uplink to purchase whatever you may need.",
    "We provide you with these crystals the moment you send the",
    "target up to us, which can be collected at anytime through",
    "this system.",
    "",
    "Targets extracted will be ransomed back to the station once",
    "their use to us is fulfilled, with us providing you a small",
    "percentage cut. You may want to be mindful of them",
    "identifying you when they come back. We provide you with",
    "a standard contractor loadout, which will help cover your",
    "identity.",
  ];

  const errorPane = !!data.error && (
    <Modal backgroundColor="red">
      <Flex align="center">
        <Flex.Item mr={2}>
          <Icon
            size={4}
            name="exclamation-triangle" />
        </Flex.Item>
        <Flex.Item
          mr={2}
          grow={1}
          textAlign="center">
          <Box
            width="260px"
            textAlign="left"
            minHeight="80px">
            {data.error}
          </Box>
          <Button
            content="Dismiss"
            onClick={() => act('PRG_clear_error')} />
        </Flex.Item>
      </Flex>
    </Modal>
  );

  if (!data.logged_in) {
    return (

        <Box
          width="100%"
          textAlign="center">
          <Button
            content="REGISTER USER"
            color="transparent"
            onClick={() => act('PRG_login')} />
        </Box>
        {!!data.error && (
          <NoticeBox>
            {data.error}
          </NoticeBox>
        )}

    );
  }

  if (data.logged_in && data.first_load) {
    return (
      <Box
        backgroundColor="rgba(0, 0, 0, 0.8)"
        minHeight="525px">
        <FakeTerminal
          allMessages={terminalMessages}
          finishedTimeout={3000}
          onFinished={() => act('PRG_set_first_load_finished')} />
      </Box>
    );
  }

  if (data.info_screen) {
    return (
      <>
        <Box
          backgroundColor="rgba(0, 0, 0, 0.8)"
          minHeight="500px">
          <FakeTerminal
            allMessages={infoEntries}
            linesPerSecond={10} />
        </Box>
        <Button
          fluid
          content="CONTINUE"
          color="transparent"
          textAlign="center"
          onClick={() => act('PRG_toggle_info')} />
      </>
    );
  }

  return (
    <>
      {errorPane}
      <SyndPane />
    </>
  );
};

export const StatusPane = (props, context) => {
  const { act, data } = useBackend(context);

  return (
    <Section
      title={(
        <>
          Contractor Status
          <Button
            content="View Information Again"
            color="transparent"
            mb={0}
            ml={1}
            onClick={() => act('PRG_toggle_info')} />
        </>
      )}
      buttons={(
        <Box bold mr={1}>
          {data.contract_rep} Rep
        </Box>
      )}>
      <Grid>
        <Grid.Column size={0.85}>
          <LabeledList>
            <LabeledList.Item
              label="TC Available"
              buttons={(
                <Button
                  content="Claim"
                  disabled={data.redeemable_tc <= 0}
                  onClick={() => act('PRG_redeem_TC')} />
              )}>
              {data.redeemable_tc}
            </LabeledList.Item>
            <LabeledList.Item label="TC Earned">
              {data.earned_tc}
            </LabeledList.Item>
          </LabeledList>
        </Grid.Column>
        <Grid.Column>
          <LabeledList>
            <LabeledList.Item label="Contracts Completed">
              {data.contracts_completed}
            </LabeledList.Item>
            <LabeledList.Item label="Current Status">
              ACTIVE
            </LabeledList.Item>
          </LabeledList>
        </Grid.Column>
      </Grid>

  );
};

export const SyndPane = (props, context) => {
  const [tab, setTab] = useLocalState(context, 'tab', 1);
  return (
    <>
      <StatusPane state={props.state} />
      <Tabs>
        <Tabs.Tab
          selected={tab === 1}
          onClick={() => setTab(1)}>
          Contracts
        </Tabs.Tab>
        <Tabs.Tab
          selected={tab === 2}
          onClick={() => setTab(2)}>
          Hub
        </Tabs.Tab>
      </Tabs>
      {tab === 1 && (
        <ContractsTab />
      )}
      {tab === 2 && (
        <HubTab />
      )}
    </>
  );
};

const ContractsTab = (props, context) => {
  const { act, data } = useBackend(context);
  const contracts = data.contracts || [];
  return (
    <>
      <Section
        title="Available Contracts"
        buttons={(
          <Button
            content="Call Extraction"
            disabled={!data.ongoing_contract || data.extraction_enroute}
            onClick={() => act('PRG_call_extraction')} />
        )}>
        {contracts.map(contract => {
          if (data.ongoing_contract
            && contract.status !== CONTRACT_STATUS_ACTIVE) {
            return;
          }
          const active = (contract.status > CONTRACT_STATUS_INACTIVE);
          if (contract.status >= CONTRACT_STATUS_COMPLETE) {
            return;
          }
          return (
            <Section
              key={contract.target}
              title={contract.target
                ? `${contract.target} (${contract.target_rank})`
                : "Invalid Target"}
              level={active ? 1 : 2}
              buttons={(
                <>
                  <Box
                    inline
                    bold
                    mr={1}>
                    {contract.payout} (+{contract.payout_bonus}) TC
                  </Box>
                  <Button
                    content={active ? "Abort" : "Accept"}
                    disabled={contract.extraction_enroute}
                    color={active && "bad"}
                    onClick={() => act(
                      'PRG_contract' + (active ? '_abort' : '-accept'),
                      {
                        contract_id: contract.id,
                      })} />
                </>
              )}>
              <Grid>
                <Grid.Column>
                  {contract.message}
                </Grid.Column>
                <Grid.Column size={0.5}>
                  <Box
                    bold
                    mb={1}>
                    Dropoff Location:
                  </Box>
                  <Box>
                    {contract.dropoff}
                  </Box>
                </Grid.Column>
              </Grid>

          );
        })}

        <Box bold>
          {data.dropoff_direction}
        </Box>

    </>
  );
};

const HubTab = (props, context) => {
  const { act, data } = useBackend(context);
  const contractor_hub_items = data.contractor_hub_items || [];
  return (

      {contractor_hub_items.map(item => {
        const repInfo = item.cost ? (item.cost + ' Rep') : 'FREE';
        const limited = (item.limited !== -1);
        return (
          <Section
            key={item.name}
            title={item.name + ' - ' + repInfo}
            level={2}
            buttons={(
              <>
                {limited && (
                  <Box inline bold mr={1}>
                    {item.limited} remaining
                  </Box>
                )}
                <Button
                  content="Purchase"
                  disabled={data.contract_rep < item.cost
                    || (limited && item.limited <= 0)}
                  onClick={() => act('buy_hub', {
                    item: item.name,
                    cost: item.cost,
                  })} />
              </>
            )}>

                  <Icon
                    fontSize="60px"
                    name={item.item_icon} />

                  {item.desc}

        );
      })}

  );
};


code:
import { useBackend, useSharedState } from '../backend';
import { Button, Flex, LabeledList, NoticeBox, Section, Tabs } from '../components';
import { Window } from '../layouts';

export const TachyonArray = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    records = [],
    is_research,
  } = data;
  return (
    <Window
      width={550}
      height={250}
      resizable>
      <Window.Content scrollable>
        <Flex direction="column" height="100%">
          <Flex.Item grow={1} className="TachyonArray__Content">
            {!records.length ? (
              <NoticeBox>
                No Records
              </NoticeBox>
            ) : (
              <TachyonArrayContent />
            )}
          </Flex.Item>
          {!!is_research && (
            <Flex.Item className="TachyonArray__ResearchFooter">
              <Button
                onClick={() => act("open_experiments")}
                icon="tasks">
                View Experiments
              </Button>
            </Flex.Item>
          )}
        </Flex>
      </Window.Content>
    </Window>
  );
};

export const TachyonArrayContent = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    records = [],
  } = data;
  const [
    activeRecordName,
    setActiveRecordName,
  ] = useSharedState(context, 'record', records[0]?.name);
  const activeRecord = records.find(record => {
    return record.name === activeRecordName;
  });
  return (
    <Section height="100%">
      <Flex>
        <Flex.Item>
          <Tabs vertical>
            {records.map(record => (
              <Tabs.Tab
                icon="file"
                key={record.name}
                selected={record.name === activeRecordName}
                onClick={() => setActiveRecordName(record.name)}>
                {record.name}
              </Tabs.Tab>
            ))}
          </Tabs>
        </Flex.Item>
        {activeRecord ? (
          <Flex.Item className="TachyonArray__ActiveRecord">
            <Section
              level="2"
              title={activeRecord.name}
              buttons={(
                <>
                  <Button.Confirm
                    icon="trash"
                    content="Delete"
                    color="bad"
                    onClick={() => act('delete_record', {
                      'ref': activeRecord.ref,
                    })} />
                  <Button
                    icon="print"
                    content="Print"
                    onClick={() => act('print_record', {
                      'ref': activeRecord.ref,
                    })} />
                </>
              )}>
              <LabeledList>
                <LabeledList.Item label="Timestamp">
                  {activeRecord.timestamp}
                </LabeledList.Item>
                <LabeledList.Item label="Coordinates">
                  {activeRecord.coordinates}
                </LabeledList.Item>
                <LabeledList.Item label="Displacement">
                  {activeRecord.displacement} seconds
                </LabeledList.Item>
                <LabeledList.Item label="Epicenter Radius">
                  {activeRecord.factual_epicenter_radius}
                  {activeRecord.theory_epicenter_radius
                  && " (Theoretical: "
                  + activeRecord.theory_epicenter_radius + ")"}
                </LabeledList.Item>
                <LabeledList.Item label="Outer Radius">
                  {activeRecord.factual_outer_radius}
                  {activeRecord.theory_outer_radius
                  && " (Theoretical: "
                  + activeRecord.theory_outer_radius + ")"}
                </LabeledList.Item>
                <LabeledList.Item label="Shockwave Radius">
                  {activeRecord.factual_shockwave_radius}
                  {activeRecord.theory_shockwave_radius
                  && " (Theoretical: "
                  + activeRecord.theory_shockwave_radius + ")"}
                </LabeledList.Item>
              </LabeledList>
            </Section>
          </Flex.Item>
        ) : (
          <Flex.Item grow={1} basis={0}>
            <NoticeBox>
              No Record Selected
            </NoticeBox>
          </Flex.Item>
        )}
      </Flex>
    </Section>
  );
};


code:
import { toFixed } from 'common/math';
import { useBackend } from '../backend';
import { Button, LabeledControls, NumberInput, RoundGauge, Section } from '../components';
import { formatSiUnit } from '../format';
import { Window } from '../layouts';

const formatPressure = value => {
  if (value < 10000) {
    return toFixed(value) + ' kPa';
  }
  return formatSiUnit(value * 1000, 1, 'Pa');
};

export const Tank = (props, context) => {
  const { act, data } = useBackend(context);
  const {
    defaultReleasePressure,
    minReleasePressure,
    maxReleasePressure,
    leakPressure,
    fragmentPressure,
    tankPressure,
    releasePressure,
    connected,
  } = data;
  return (
    <Window
      width={275}
      height={120}>
      <Window.Content>
        <Section>
          <LabeledControls>
            <LabeledControls.Item label="Pressure">
              <RoundGauge
                value={tankPressure}
                minValue={0}
                maxValue={fragmentPressure * 1.15}
                alertAfter={leakPressure}
                ranges={{
                  "good": [0, leakPressure],
                  "average": [leakPressure, fragmentPressure],
                  "bad": [fragmentPressure, fragmentPressure * 1.15],
                }}
                format={formatPressure}
                size={2} />
            </LabeledControls.Item>
            <LabeledControls.Item label="Pressure Regulator">
              <Button
                icon="fast-backward"
                disabled={data.ReleasePressure === data.minReleasePressure}
                onClick={() => act('pressure', {
                  pressure: 'min',
                })} />
              <NumberInput
                animated
                value={parseFloat(data.releasePressure)}
                width="65px"
                unit="kPa"
                minValue={data.minReleasePressure}
                maxValue={data.maxReleasePressure}
                onChange={(e, value) => act('pressure', {
                  pressure: value,
                })} />
              <Button
                icon="fast-forward"
                disabled={data.ReleasePressure === data.maxReleasePressure}
                onClick={() => act('pressure', {
                  pressure: 'max',
                })} />
              <Button
                icon="undo"
                content=""
                disabled={data.ReleasePressure === data.defaultReleasePressure}
                onClick={() => act('pressure', {
                  pressure: 'reset',
                })} />
            </LabeledControls.Item>
          </LabeledControls>
        </Section>
      </Window.Content>
    </Window>
  );
};


code:
import { useBackend } from '../backend';
import { Button, LabeledList, Section } from '../components';
import { Window } from '../layouts';

export const TankDispenser = (props, context) => {
  const { act, data } = useBackend(context);
  return (
    <Window
      width={275}
      height={103}>
      <Window.Content>
        <Section>
          <LabeledList>
            <LabeledList.Item
              label="Plasma"
              buttons={(
                <Button
                  icon={data.plasma ? 'square' : 'square-o'}
                  content="Dispense"
                  disabled={!data.plasma}
                  onClick={() => act('plasma')} />
              )}>
              {data.plasma}
            </LabeledList.Item>
            <LabeledList.Item
              label="Oxygen"
              buttons={(
                <Button
                  icon={data.oxygen ? 'square' : 'square-o'}
                  content="Dispense"
                  disabled={!data.oxygen}
                  onClick={() => act('oxygen')} />
              )}>
              {data.oxygen}
            </LabeledList.Item>
          </LabeledList>
        </Section>
      </Window.Content>
    </Window>
  );
};


code:
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.",
};
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">

Console Locked

            <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">

Console Locked

            <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:

                {Object.keys(points).map(k => (

                    {k}: {points[k]}
                    {!!points_last_tick[k] && (
                      ` (+${points_last_tick[k]}/sec)`
                    )}

                ))}

            </Box>
            <Box>
              Security protocols:
              <span
                className={`Techweb__SecProtocol ${!!sec_protocols && "engaged"}`}>
                {sec_protocols ? "Engaged" : "Disengaged"}

            </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 {design_cache[x].name}:

              <span
                className={`${design_cache[x].class} Techweb__DesignIcon`} />
            </>
          )}

      ))}
    </>
  );
};

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>
      )}

  );
};

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>
  );
};