code:
import { classes } from "common/react";
import { sendAct, useBackend, useLocalState } from "../../backend";
import { Autofocus, Box, Button, Flex, LabeledList, Popper, Stack, TrackOutsideClicks } from "../../components";
import { createSetPreference, PreferencesMenuData, RandomSetting } from "./data";
import { CharacterPreview } from "./CharacterPreview";
import { RandomizationButton } from "./RandomizationButton";
import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";
import { MultiNameInput, NameInput } from "./names";
import { Gender, GENDERS } from "./preferences/gender";
import { CharacterPreviewType } from "./preferences/features/game_preferences/preview_type";
import features from "./preferences/features";
import { FeatureChoicedServerData, FeatureValueInput } from "./preferences/features/base";
import { filterMap, sortBy } from "common/collections";
import { useRandomToggleState } from "./useRandomToggleState";
import { Genital, GenitalsButton } from "./preferences/features/character_preferences/genitals";

const CLOTHING_CELL_SIZE = 48;
const CLOTHING_SIDEBAR_ROWS = 9;

const CLOTHING_SELECTION_CELL_SIZE = 48;
const CLOTHING_SELECTION_WIDTH = 5.4;
const CLOTHING_SELECTION_MULTIPLIER = 5.2;

const CharacterControls = (props: {
  handleRotate: () => void,
  handleRotateLeft: () => void,
  handleRotateRight: () => void,
  handleOpenSpecies: () => void,
  gender: Gender,
  setGender: (gender: Gender) => void,
  showGender: boolean,
  genitals: Genital,
  setGenitals: (genitals: Genital) => void,
  showGenitals: boolean,
}) => {
  return (
    <Stack>
      <Stack.Item>
        <Button
          onClick={props.handleRotateLeft}
          fontSize="22px"
          icon="undo"
          tooltip="Rotate Left"
          tooltipPosition="top"
        />
      </Stack.Item>

      <Stack.Item>
        <Button
          onClick={props.handleOpenSpecies}
          fontSize="22px"
          icon="paw"
          tooltip="Species"
          tooltipPosition="top"
        />
      </Stack.Item>

      {props.showGender && (
        <Stack.Item>
          <GenderButton
            gender={props.gender}
            handleSetGender={props.setGender}
          />
        </Stack.Item>
      )}

      {props.showGenitals && (
        <Stack.Item>
          <GenitalsButton
            genitals={props.genitals}
            handleSetGenitals={props.setGenitals}
          />
        </Stack.Item>
      )}

      <Stack.Item>
        <Button
          onClick={props.handleRotateRight}
          fontSize="22px"
          icon="redo"
          tooltip="Rotate Right"
          tooltipPosition="top"
        />
      </Stack.Item>

    </Stack>
  );
};

const ChoicedSelection = (props: {
  name: string,
  catalog: FeatureChoicedServerData,
  selected: string,
  supplementalFeature?: string,
  supplementalValue?: unknown,
  onClose: () => void,
  onSelect: (value: string) => void,
}, context) => {
  const { act } = useBackend<PreferencesMenuData>(context);

  const {
    catalog,
    supplementalFeature,
    supplementalValue,
  } = props;

  if (!catalog.icons) {
    return (
      <Box color="red">
        Provided catalog had no icons!
      </Box>
    );
  }

  return (
    <Box style={{
      background: "white",
      padding: "5px",

      height:
      `${CLOTHING_SELECTION_CELL_SIZE * CLOTHING_SELECTION_MULTIPLIER}px`,
      width: `${CLOTHING_SELECTION_CELL_SIZE * CLOTHING_SELECTION_WIDTH * 2}px`,
    }}>
      <Stack vertical fill>
        <Stack.Item>
          <Stack fill>
            {supplementalFeature && (
              <Stack.Item>
                <FeatureValueInput
                  act={act}
                  feature={features[supplementalFeature]}
                  featureId={supplementalFeature}
                  shrink
                  value={supplementalValue}
                />
              </Stack.Item>
            )}

            <Stack.Item grow>
              <Box style={{
                "border-bottom": "1px solid #888",
                "font-weight": "bold",
                "font-size": "14px",
                "text-align": "center",
              }}>
                Select {props.name.toLowerCase()}
              </Box>
            </Stack.Item>

            <Stack.Item>
              <Button
                color="red"
                onClick={props.onClose}
              >
                X
              </Button>
            </Stack.Item>
          </Stack>
        </Stack.Item>

        <Stack.Item overflowX="hidden" overflowY="scroll">
          <Autofocus>
            <Flex wrap>
              {Object.entries(catalog.icons).map(([name, image], index) => {
                return (
                  <Flex.Item
                    mr={2}
                    key={index}
                    basis={`${CLOTHING_SELECTION_CELL_SIZE}px`}
                    style={{
                      padding: "5px",
                    }}
                  >
                    <Button
                      onClick={() => {
                        props.onSelect(name);
                      }}
                      selected={name === props.selected}
                      tooltip={name}
                      tooltipPosition="right"
                      style={{
                        height: `${CLOTHING_SELECTION_CELL_SIZE}px`,
                        width: `${CLOTHING_SELECTION_CELL_SIZE}px`,
                      }}
                    >
                      <Box className={classes(["preferences32x32", image, "centered-image"])} />
                    </Button>
                    <Box>
                      {name}
                    </Box>
                  </Flex.Item>
                );
              })}
            </Flex>
          </Autofocus>
        </Stack.Item>
      </Stack>
    </Box>
  );
};

const GenderButton = (props: {
  handleSetGender: (gender: Gender) => void,
  gender: Gender,
}, context) => {
  const [genderMenuOpen, setGenderMenuOpen] = useLocalState(context, "genderMenuOpen", false);

  return (
    <Popper options={{
      placement: "right-end",
    }} popperContent={(
      genderMenuOpen
        && (
          <Stack backgroundColor="white" ml={0.5} p={0.3}>
            {[Gender.Male, Gender.Female].map(gender => {
              return (
                <Stack.Item key={gender}>
                  <Button
                    selected={gender === props.gender}
                    onClick={() => {
                      props.handleSetGender(gender);
                      setGenderMenuOpen(false);
                    }}
                    fontSize="22px"
                    icon={GENDERS[gender].icon}
                    tooltip={GENDERS[gender].text}
                    tooltipPosition="top"
                  />
                </Stack.Item>
              );
            })}
          </Stack>
        )
    )}>
      <Button
        onClick={() => {
          setGenderMenuOpen(!genderMenuOpen);
        }}
        fontSize="22px"
        icon={GENDERS[props.gender].icon}
        tooltip="Gender"
        tooltipPosition="top"
      />
    </Popper>
  );
};

const MainFeature = (props: {
  catalog: FeatureChoicedServerData & {
    name: string,
    supplemental_feature?: string,
  },
  currentValue: string,
  isOpen: boolean,
  handleClose: () => void,
  handleOpen: () => void,
  handleSelect: (newClothing: string) => void,
  randomization?: RandomSetting,
  setRandomization: (newSetting: RandomSetting) => void,
}, context) => {
  const { act, data } = useBackend<PreferencesMenuData>(context);

  const {
    catalog,
    currentValue,
    isOpen,
    handleOpen,
    handleClose,
    handleSelect,
    randomization,
    setRandomization,
  } = props;

  const supplementalFeature = catalog.supplemental_feature;
  if (!catalog?.name) {
    return (No catalog for {currentValue}!);
  }
  return (
    <Popper options={{
      placement: "bottom-start",
    }} popperContent={isOpen && (
      <TrackOutsideClicks onOutsideClick={props.handleClose}>
        <ChoicedSelection
          name={catalog.name}
          catalog={catalog}
          selected={currentValue}
          supplementalFeature={supplementalFeature}
          supplementalValue={supplementalFeature && (
            data
              .character_preferences
              .supplemental_features[supplementalFeature]
          )}
          onClose={handleClose}
          onSelect={handleSelect}
        />
      </TrackOutsideClicks>
    )}>
      <Button
        onClick={() => {
          if (isOpen) {
            handleClose();
          } else {
            handleOpen();
          }
        }}
        style={{
          height: `${CLOTHING_CELL_SIZE}px`,
          width: `${CLOTHING_CELL_SIZE}px`,
        }}
        position="relative"
        tooltip={catalog.name}
        tooltipPosition="right"
      >
        <Box
          className={classes([
            "preferences32x32",
              catalog.icons![currentValue],
              "centered-image",
          ])}
          style={{
            transform: randomization
              ? "translateX(-70%) translateY(-70%) scale(1.1)"
              : "translateX(-50%) translateY(-50%) scale(1.3)",
          }}
        />

        {(randomization && <RandomizationButton
          dropdownProps={{
            dropdownStyle: {
              bottom: 0,
              position: "absolute",
              right: "1px",
            },

            onOpen: (event) => {
              // We're a button inside a button.
              // Did you know that's against the W3C standard? :)
              event.cancelBubble = true;
              event.stopPropagation();
            },
          }}
          value={randomization}
          setValue={setRandomization}
        />)}
      </Button>
    </Popper>
  );
};

const createSetRandomization = (
  act: typeof sendAct,
  preference: string,
) => (newSetting: RandomSetting) => {
  act("set_random_preference", {
    preference,
    value: newSetting,
  });
};

const sortPreferences = sortBy<[string, unknown]>(
  ([featureId, _]) => {
    const feature = features[featureId];
    return feature?.name;
  });

const PreferenceList = (props: {
  act: typeof sendAct,
  preferences: Record<string, unknown>,
  randomizations: Record<string, RandomSetting>,
}) => {
  return (
    <Stack.Item basis="50%" grow style={{
      background: "rgba(0, 0, 0, 0.5)",
      padding: "4px",
    }} overflowX="hidden" overflowY="scroll">
      <LabeledList>
        {
          sortPreferences(Object.entries(props.preferences))
            .map(([featureId, value]) => {
              const feature = features[featureId];
              const randomSetting = props.randomizations[featureId];

              if (feature === undefined) {
                return (
                  <Stack.Item key={featureId}>
                    Feature {featureId} is not recognized.
                  </Stack.Item>
                );
              }

              return (
                <LabeledList.Item
                  key={featureId}
                  label={feature.name}
                  verticalAlign="middle"
                >
                  <Stack fill>
                    {randomSetting && (
                      <Stack.Item>
                        <RandomizationButton
                          setValue={createSetRandomization(
                            props.act,
                            featureId,
                          )}
                          value={randomSetting}
                        />
                      </Stack.Item>
                    )}

                    <Stack.Item grow>
                      <FeatureValueInput
                        act={props.act}
                        feature={feature}
                        featureId={featureId}
                        value={value}
                      />
                    </Stack.Item>
                  </Stack>
                </LabeledList.Item>
              );
            })
        }
      </LabeledList>
    </Stack.Item>
  );
};

export const MainPage = (props: {
  openSpecies: () => void,
}, context) => {
  const { act, data } = useBackend<PreferencesMenuData>(context);
  const [currentClothingMenu, setCurrentClothingMenu]
    = useLocalState<string | null>(context, "currentClothingMenu", null);
  const [multiNameInputOpen, setMultiNameInputOpen]
    = useLocalState(context, "multiNameInputOpen", false);
  const [randomToggleEnabled] = useRandomToggleState(context);

  return (
    <ServerPreferencesFetcher render={(serverData) => {
      const currentSpeciesData = serverData && serverData.species[
        data
          .character_preferences
          .misc
          .species
      ];

      const contextualPreferences
        = data.character_preferences.secondary_features || [];

      const game_preferences
        = data.character_preferences.game_preferences || [];

      const mainFeatures = [
        ...Object.entries(data.character_preferences.features)
          .filter(([featureName]) => {
            if (!currentSpeciesData) {
              return false;
            }

            return currentSpeciesData
              .enabled_features
              .indexOf(featureName) !== -1;
          }),
      ];

      const randomBodyEnabled
        = (data.character_preferences.non_contextual.random_body
            !== RandomSetting.Disabled)
          || randomToggleEnabled;

      const getRandomization = (
        preferences: Record<string, unknown>
      ): Record<string, RandomSetting> => {
        if (!serverData) {
          return {};
        }

        return Object.fromEntries(filterMap(
          Object.keys(preferences), preferenceKey => {
            if (serverData.random.randomizable.indexOf(preferenceKey) === -1) {
              return undefined;
            }

            if (!randomBodyEnabled) {
              return undefined;
            }

            return [
              preferenceKey,
              data.character_preferences.randomization[preferenceKey]
                || RandomSetting.Disabled,
            ];
          }));
      };

      const randomizationOfMainFeatures
        = getRandomization(Object.fromEntries(mainFeatures));

      const nonContextualPreferences
        = {
          ...data.character_preferences.non_contextual,
        };

      if (randomBodyEnabled) {
        nonContextualPreferences["random_species"]
          = data.character_preferences.randomization["species"];
      } else {
        // We can't use random_name/is_accessible because the
        // server doesn't know whether the random toggle is on.
        delete nonContextualPreferences["random_name"];
      }

      return (
        <>
          {multiNameInputOpen && <MultiNameInput
            handleClose={() => setMultiNameInputOpen(false)}
            handleRandomizeName={preference => act("randomize_name", {
              preference,
            })}
            handleUpdateName={(nameType, value) => act("set_preference", {
              preference: nameType,
              value,
            })}
            names={data.character_preferences.names}
          />}

          <Stack height={`${CLOTHING_SIDEBAR_ROWS * CLOTHING_CELL_SIZE}px`}>
            <Stack.Item fill>
              <Stack vertical fill>
                <Stack.Item>
                  <CharacterControls
                    gender={data.character_preferences.misc.gender}
                    setGender={createSetPreference(act, "gender")}
                    showGender={
                      currentSpeciesData ? !!currentSpeciesData.sexes : true
                    }
                    genitals={data.character_preferences.misc.genitals}
                    setGenitals={createSetPreference(act, "genitals")}
                    showGenitals={
                      currentSpeciesData ? !!currentSpeciesData.sexes : true
                    }
                    handleOpenSpecies={props.openSpecies}
                    handleRotate={() => {
                      act("rotate");
                    }}
                    handleRotateLeft={() => {
                      act("rotateleft");
                    }}
                    handleRotateRight={() => {
                      act("rotateright");
                    }}
                  />
                </Stack.Item>

                <Stack.Item grow>
                  <CharacterPreview
                    height="100%"
                    id={data.character_preview_view} />
                </Stack.Item>

                <Stack.Item position="relative">
                  <CharacterPreviewType
                    character_preview_type={data.character_preview_type}
                    handlePreviewJob={() => act('select_preview_type', {
                      new_preview_type: 'Job',
                    })}
                    handlePreviewNaked={() => act('select_preview_type', {
                      new_preview_type: 'Naked',
                    })}
                  />
                </Stack.Item>

                <Stack.Item position="relative">
                  <NameInput
                    name={
                      data.character_preferences.names[data.name_to_use]
                    }
                    handleUpdateName={
                      createSetPreference(act, data.name_to_use)
                    }
                    openMultiNameInput={() => {
                      setMultiNameInputOpen(true);
                    }}
                  />
                </Stack.Item>

              </Stack>
            </Stack.Item>

            <Stack.Item
              fill
              width={`${(CLOTHING_CELL_SIZE * 2) + 15}px`}
            >
              <Stack height="100%" vertical wrap>
                {mainFeatures
                  .map(([clothingKey, clothing]) => {
                    const catalog = (
                      serverData
                          && serverData[clothingKey] as
                            FeatureChoicedServerData & {
                              name: string,
                            }
                    );

                    return catalog && (
                      <Stack.Item key={clothingKey} mt={0.5} px={0.5}>
                        <MainFeature
                          catalog={catalog}
                          currentValue={clothing}
                          isOpen={
                            currentClothingMenu === clothingKey
                          }
                          handleClose={() => {
                            setCurrentClothingMenu(null);
                          }}
                          handleOpen={() => {
                            setCurrentClothingMenu(clothingKey);
                          }}
                          handleSelect={createSetPreference(act, clothingKey)}
                          randomization={
                            randomizationOfMainFeatures[clothingKey]
                          }
                          setRandomization={createSetRandomization(
                            act,
                            clothingKey,
                          )}
                        />
                      </Stack.Item>
                    );
                  })}
              </Stack>
            </Stack.Item>

            <Stack.Item grow basis={0}>
              <Stack vertical fill>
                <PreferenceList
                  act={act}
                  randomizations={getRandomization(contextualPreferences)}
                  preferences={contextualPreferences}
                />

                <PreferenceList
                  act={act}
                  randomizations={getRandomization(nonContextualPreferences)}
                  preferences={nonContextualPreferences}
                />
              </Stack>
            </Stack.Item>

          </Stack>
        </>
      );
    }} />
  );
};


code:
import { Section, Button, Box, ColorBox, Dropdown, Stack } from "../../components";
import { useBackend } from "../../backend";
import { Marking, MarkingZone, PreferencesMenuData } from "./data";
import { CharacterPreview } from "./CharacterPreview";
import { CharacterPreviewType } from "./preferences/features/game_preferences/preview_type";

const MarkingInput = (props: {
  our_zone: MarkingZone
  marking: Marking
}, context) => {
  const { act, data } = useBackend<PreferencesMenuData>(context);
  return (
    <Box
      textAlign="center"
      verticalAlign="center"
      width="100%">
      <Button
        width="25%"
        onClick={() => act("color_marking", {
          body_zone: props.our_zone.body_zone,
          marking_name: props.marking.name,
          marking_index: props.marking.marking_index,
        })}
      >
        <ColorBox color={props.marking.color} />
      </Button>
      <Button
        width="25%"
        icon="sort-up"
        onClick={() => act("move_marking_up", {
          body_zone: props.our_zone.body_zone,
          marking_name: props.marking.name,
          marking_index: props.marking.marking_index,
        })}
      />
      <Button
        width="25%"
        icon="sort-down"
        onClick={() => act("move_marking_down", {
          body_zone: props.our_zone.body_zone,
          marking_name: props.marking.name,
          marking_index: props.marking.marking_index,
        })}
      />
      <Button
        icon="times"
        color="bad"
        onClick={() => act("remove_marking", {
          body_zone: props.our_zone.body_zone,
          marking_name: props.marking.name,
          marking_index: props.marking.marking_index,
        })}
      />
    </Box>
  );
};

const ZoneItem = (props: {
  our_zone: MarkingZone
}, context) => {
  const { act, data } = useBackend<PreferencesMenuData>(context);
  const maxmarkings = data.maximum_markings_per_limb;
  return (
    <Stack.Item>
      <Section title={props.our_zone.name + " (" + props.our_zone.markings.length + "/" + maxmarkings + ")"}>
        <Stack vertical>
          <Stack.Item>
            {props.our_zone.markings.map(marking => (
              <Stack key={marking.marking_index}>
                <Stack.Item width="50%" mb={1}>
                  <Dropdown
                    width="100%"
                    options={props.our_zone.markings_choices}
                    displayText={marking.name}
                    onSelected={(new_marking) => act("change_marking", {
                      body_zone: props.our_zone.body_zone,
                      marking_index: marking.marking_index,
                      new_marking: new_marking,
                    })}
                  />
                </Stack.Item>
                <Stack.Item width="50%">
                  <MarkingInput
                    our_zone={props.our_zone}
                    marking={marking}
                  />
                </Stack.Item>
              </Stack>
            ))}
          </Stack.Item>
          <Stack.Item>
            {(props.our_zone.can_add_markings) && (
              <Button
                icon="plus"
                color="good"
                onClick={() => act("add_marking", {
                  body_zone: props.our_zone.body_zone,
                })}
              />
            ) || (
              <Box>
                Marking limit reached!
              </Box>
            )}
          </Stack.Item>
        </Stack>
      </Section>
    </Stack.Item>
  );
};

export const MarkingsPage = (props, context, markingslist: MarkingZone[]) => {
  const { act, data } = useBackend<PreferencesMenuData>(context);
  markingslist = [];
  markingslist.length = data.marking_parts.length;
  for (let index = 0; index < data.marking_parts.length; index++) {
    markingslist[index] = data.marking_parts[index];
  }
  const firststack = markingslist.splice(0, 6);
  firststack.length = Math.min(firststack.length, 6);
  const secondstack = markingslist.splice(0, 6);
  secondstack.length = Math.min(firststack.length, 6);
  const maxmarkings = data.maximum_markings_per_limb;
  return (
    <Stack
      vertical>
      <Stack.Item>
        <Dropdown
          width="75%"
          displayText="Presets"
          options={data.body_marking_sets}
          onSelected={(value) => act("set_preset", { preset: value })}
        />
      </Stack.Item>
      <Stack.Item>
        <Stack
          vertical={false}>
          <Stack.Item
            minWidth="38%"
            height={40}>
            <Section
              overflowX="hidden"
              overflowY="scroll"
              fill>
              <Stack vertical>
                {firststack.map(zone => (
                  <ZoneItem
                    key={zone.body_zone}
                    our_zone={zone}
                  />
                ))}
              </Stack>
            </Section>
          </Stack.Item>
          <Stack.Item
            minWidth="38%"
            height={40}>
            <Section
              overflowX="hidden"
              overflowY="scroll"
              fill>
              <Stack vertical>
                {secondstack.map(zone => (
                  <ZoneItem
                    key={zone.body_zone}
                    our_zone={zone}
                  />
                ))}
              </Stack>
            </Section>
          </Stack.Item>
          <Stack.Item
            minWidth="20%"
            height={40}>
            <Box
              textAlign="center">
              <Button
                width="47%"
                onClick={() => {
                  act("rotateleft");
                }}
                textAlign="center"
                fontSize="22px"
                icon="undo"
                tooltip="Rotate Left"
                tooltipPosition="top"
              />
              <Button
                width="47%"
                onClick={() => {
                  act("rotateright");
                }}
                textAlign="center"
                fontSize="22px"
                icon="redo"
                tooltip="Rotate Right"
                tooltipPosition="top"
              />
            </Box>
            <CharacterPreview
              height="75%"
              id={data.character_preview_view}
            />
            <CharacterPreviewType
              character_preview_type={data.character_preview_type}
              handlePreviewJob={() => act('select_preview_type', {
                new_preview_type: 'Job',
              })}
              handlePreviewNaked={() => act('select_preview_type', {
                new_preview_type: 'Naked',
              })}
            />
          </Stack.Item>
        </Stack>
      </Stack.Item>
    </Stack>
  );
};

repo_name: AnalWerewolf/sundown

license: mit


code:
import { InfernoNode } from "inferno";
import { Button } from "../../components";

export const PageButton = <P extends unknown>(props: {
  currentPage: P,
  page: P,
  otherActivePages?: P[],

  setPage: (page: P) => void,

  children?: InfernoNode,
}) => {
  const pageIsActive = props.currentPage === props.page
    || (
      props.otherActivePages
        && props.otherActivePages.indexOf(props.currentPage) !== -1
    );

  return (
    <Button
      align="center"
      fontSize="1.2em"
      fluid
      selected={pageIsActive}
      onClick={() => props.setPage(props.page)}
    >
      {props.children}
    </Button>
  );
};


code:
import { StatelessComponent } from "inferno";
import { Box, Icon, Stack, Tooltip } from "../../components";
import { PreferencesMenuData, Quirk } from "./data";
import { useBackend, useLocalState } from "../../backend";
import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";

const getValueClass = (value: number): string => {
  if (value > 0) {
    return "positive";
  } else if (value < 0) {
    return "negative";
  } else {
    return "neutral";
  }
};

const QuirkList = (props: {
  quirks: [string, Quirk & {
    failTooltip?: string;
  }][],
  onClick: (quirkName: string, quirk: Quirk) => void,
}) => {
  return (
    // Stack is not used here for a variety of IE flex bugs
    <Box className="PreferencesMenu__Quirks__QuirkList">
      {props.quirks.map(([quirkKey, quirk]) => {
        const className = "PreferencesMenu__Quirks__QuirkList__quirk";

        const child = (
          <Box
            className={className}
            key={quirkKey}
            role="button"
            tabIndex="1"
            onClick={() => {
              props.onClick(quirkKey, quirk);
            }}
          >
            <Stack fill>
              <Stack.Item align="center" style={{
                "min-width": "15%",
                "max-width": "15%",
                "text-align": "center",
              }}>
                <Icon
                  color="#333"
                  fontSize={3}
                  name={quirk.icon}
                />
              </Stack.Item>

              <Stack.Item align="stretch" style={{
                "border-right": "1px solid black",
                "margin-left": 0,
              }} />

              <Stack.Item grow style={{
                "margin-left": 0,

                // Fixes an IE bug for text overflowing in Flex boxes
                "min-width": "0%",
              }}>
                <Stack vertical fill>
                  <Stack.Item
                    className={
                      `${className}--${getValueClass(quirk.value)}`
                    }
                    style={{
                      "border-bottom": "1px solid black",
                      "padding": "2px",
                    }}
                  >
                    <Stack fill style={{
                      "font-size": "1.2em",
                    }}>
                      <Stack.Item grow basis="content">
                        {quirk.name}
                      </Stack.Item>

                      <Stack.Item>
                        {quirk.value}
                      </Stack.Item>
                    </Stack>
                  </Stack.Item>

                  <Stack.Item grow basis="content" style={{
                    "margin-top": 0,
                    "padding": "3px",
                  }}>
                    {quirk.description}
                  </Stack.Item>
                </Stack>
              </Stack.Item>
            </Stack>
          </Box>
        );

        if (quirk.failTooltip) {
          return (
            <Tooltip content={quirk.failTooltip}>
              {child}
            </Tooltip>
          );
        } else {
          return child;
        }
      })}
    </Box>
  );
};

const StatDisplay: StatelessComponent<{}> = (props) => {
  return (
    <Box
      backgroundColor="#eee"
      bold
      color="black"
      fontSize="1.2em"
      px={3}
      py={0.5}
    >
      {props.children}
    </Box>
  );
};

export const QuirksPage = (props, context) => {
  const { act, data } = useBackend<PreferencesMenuData>(context);

  const [selectedQuirks, setSelectedQuirks] = useLocalState(
    context,
    `selectedQuirks_${data.active_slot}`,
    data.selected_quirks,
  );

  return (
    <ServerPreferencesFetcher
      render={data => {
        if (!data) {
          return <Box>Loading quirks...</Box>;
        }

        const {
          max_positive_quirks: maxPositiveQuirks,
          quirk_blacklist: quirkBlacklist,
          quirk_info: quirkInfo,
        } = data.quirks;

        const quirks = Object.entries(quirkInfo);
        quirks.sort(([_, quirkA], [__, quirkB]) => {
          if (quirkA.value === quirkB.value) {
            return (quirkA.name > quirkB.name) ? 1 : -1;
          } else {
            return quirkA.value - quirkB.value;
          }
        });

        let balance = 0;
        let positiveQuirks = 0;

        for (const selectedQuirkName of selectedQuirks) {
          const selectedQuirk = quirkInfo[selectedQuirkName];
          if (!selectedQuirk) {
            continue;
          }

          if (selectedQuirk.value > 0) {
            positiveQuirks += 1;
          }

          balance += selectedQuirk.value;
        }

        const getReasonToNotAdd = (quirkName: string) => {
          const quirk = quirkInfo[quirkName];

          if (
            quirk.value > 0
          ) {
            if (positiveQuirks >= maxPositiveQuirks) {
              return "You can't have any more positive quirks!";
            } else if (balance + quirk.value > 0) {
              return "You need a negative quirk to balance this out!";
            }
          }

          const selectedQuirkNames = selectedQuirks.map(quirkKey => {
            return quirkInfo[quirkKey].name;
          });

          for (const blacklist of quirkBlacklist) {
            if (blacklist.indexOf(quirk.name) === -1) {
              continue;
            }

            for (const incompatibleQuirk of blacklist) {
              if (
                incompatibleQuirk !== quirk.name
                && selectedQuirkNames.indexOf(incompatibleQuirk) !== -1
              ) {
                return `This is incompatible with ${incompatibleQuirk}!`;
              }
            }
          }

          return undefined;
        };

        const getReasonToNotRemove = (quirkName: string) => {
          const quirk = quirkInfo[quirkName];

          if (balance - quirk.value > 0) {
            return "You need to remove a positive quirk first!";
          }

          return undefined;
        };

        return (
          <Stack align="center" fill>
            <Stack.Item basis="50%">
              <Stack vertical fill align="center">
                <Stack.Item>
                  <Box fontSize="1.3em">
                    Positive Quirks
                  </Box>
                </Stack.Item>

                <Stack.Item>
                  <StatDisplay>
                    {positiveQuirks} / {maxPositiveQuirks}
                  </StatDisplay>
                </Stack.Item>

                <Stack.Item>
                  <Box as="b" fontSize="1.6em">
                    Available Quirks
                  </Box>
                </Stack.Item>

                <Stack.Item grow width="100%">
                  <QuirkList
                    onClick={(quirkName, quirk) => {
                      if (getReasonToNotAdd(quirkName) !== undefined) {
                        return;
                      }

                      setSelectedQuirks(selectedQuirks.concat(quirkName));

                      act("give_quirk", { quirk: quirk.name });
                    }}
                    quirks={quirks.filter(([quirkName, _]) => {
                      return selectedQuirks
                        .indexOf(quirkName) === -1;
                    }).map(([quirkName, quirk]) => {
                      return [quirkName, {
                        ...quirk,
                        failTooltip: getReasonToNotAdd(quirkName),
                      }];
                    })}
                  />
                </Stack.Item>
              </Stack>
            </Stack.Item>

            <Stack.Item>
              <Icon
                name="exchange-alt"
                size={1.5}
                ml={2}
                mr={2}
              />
            </Stack.Item>

            <Stack.Item basis="50%">
              <Stack vertical fill align="center">
                <Stack.Item>
                  <Box fontSize="1.3em">
                    Quirk Balance
                  </Box>
                </Stack.Item>

                <Stack.Item>
                  <StatDisplay>
                    {balance}
                  </StatDisplay>
                </Stack.Item>

                <Stack.Item>
                  <Box as="b" fontSize="1.6em">
                    Current Quirks
                  </Box>
                </Stack.Item>

                <Stack.Item grow width="100%">
                  <QuirkList
                    onClick={(quirkName, quirk) => {
                      if (getReasonToNotRemove(quirkName) !== undefined) {
                        return;
                      }

                      setSelectedQuirks(
                        selectedQuirks
                          .filter(otherQuirk => quirkName !== otherQuirk),
                      );

                      act("remove_quirk", { quirk: quirk.name });
                    }}
                    quirks={quirks.filter(([quirkName, _]) => {
                      return selectedQuirks
                        .indexOf(quirkName) !== -1;
                    }).map(([quirkName, quirk]) => {
                      return [quirkName, {
                        ...quirk,
                        failTooltip: getReasonToNotRemove(quirkName),
                      }];
                    })}
                  />
                </Stack.Item>
              </Stack>
            </Stack.Item>
          </Stack>
        );
      }}
    />
  );
};


code:
import { Dropdown, Icon } from "../../components";
import { RandomSetting } from "./data";
import { exhaustiveCheck } from "common/exhaustive";

export const RandomizationButton = (props: {
  dropdownProps?: Record<string, unknown>,
  setValue: (newValue: RandomSetting) => void,
  value: RandomSetting,
}) => {
  const {
    dropdownProps = {},
    setValue,
    value,
  } = props;

  let color;

  switch (value) {
    case RandomSetting.AntagOnly:
      color = "orange";
      break;
    case RandomSetting.Disabled:
      color = "red";
      break;
    case RandomSetting.Enabled:
      color = "green";
      break;
    default:
      exhaustiveCheck(value);
  }

  return (
    <Dropdown
      backgroundColor={color}
      {...dropdownProps}
      clipSelectedText={false}
      displayText={(
        <Icon
          name="dice-d20"
          mr="-0.25em"
        />
      )}
      options={[
        {
          displayText: "Do not randomize",
          value: RandomSetting.Disabled,
        },

        {
          displayText: "Always randomize",
          value: RandomSetting.Enabled,
        },

        {
          displayText: "Randomize when antagonist",
          value: RandomSetting.AntagOnly,
        },
      ]}
      nochevron
      onSelected={setValue}
      width="auto"
    />
  );
};


code:
import { Component, InfernoNode } from "inferno";
import { resolveAsset } from "../../assets";
import { fetchRetry } from "../../http";
import { ServerData } from "./data";

// Cache response so it's only sent once
let fetchServerData: Promise<ServerData> | undefined;

export class ServerPreferencesFetcher extends Component<{
  render: (serverData: ServerData | undefined) => InfernoNode,
}, {
  serverData?: ServerData;
}> {
  state = {
    serverData: undefined,
  };

  componentDidMount() {
    this.populateServerData();
  }

  async populateServerData() {
    if (!fetchServerData) {
      fetchServerData = fetchRetry(resolveAsset("preferences.json"))
        .then(response => response.json());
    }

    const preferencesData: ServerData = await fetchServerData;

    this.setState({
      serverData: preferencesData,
    });
  }

  render() {
    return this.props.render(this.state.serverData);
  }
}


code:
import { classes } from "common/react";
import { useBackend } from "../../backend";
import { BlockQuote, Box, Button, Divider, Icon, Section, Stack, Tooltip } from "../../components";
import { CharacterPreview } from "./CharacterPreview";
import { createSetPreference, Food, PreferencesMenuData, ServerData, ServerSpeciesData } from "./data";
import { Feature, Species, fallbackSpecies } from "./preferences/species/base";
import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";

const requireSpecies = require.context("./preferences/species");

const FOOD_ICONS = {
  [Food.Cloth]: "tshirt",
  [Food.Dairy]: "cheese",
  [Food.Fried]: "bacon",
  [Food.Fruit]: "apple-alt",
  [Food.Grain]: "bread-slice",
  [Food.Gross]: "trash",
  [Food.Junkfood]: "pizza-slice",
  [Food.Meat]: "hamburger",
  [Food.Nuts]: "acorn",
  [Food.Raw]: "drumstick-bite",
  [Food.Seafood]: "fish",
  [Food.Sugar]: "candy-cane",
  [Food.Toxic]: "biohazard",
  [Food.Vegetables]: "carrot",
};

const FOOD_NAMES: Record<keyof typeof FOOD_ICONS, string> = {
  [Food.Cloth]: "Clothing",
  [Food.Dairy]: "Dairy",
  [Food.Fried]: "Fried food",
  [Food.Fruit]: "Fruit",
  [Food.Grain]: "Grain",
  [Food.Gross]: "Gross food",
  [Food.Junkfood]: "Junk food",
  [Food.Meat]: "Meat",
  [Food.Nuts]: "Nuts",
  [Food.Raw]: "Raw",
  [Food.Seafood]: "Seafood",
  [Food.Sugar]: "Sugar",
  [Food.Toxic]: "Toxic food",
  [Food.Vegetables]: "Vegetables",
  [Food.Sewage]: "Sewage",
};

const IGNORE_UNLESS_LIKED: Set<Food> = new Set([
  Food.Cloth,
  Food.Gross,
  Food.Toxic,
]);

const notIn = function<T> (set: Set<T>) {
  return (value: T) => {
    return !set.has(value);
  };
};

const FoodList = (props: {
  food: Food[],
  icon: string,
  name: string,
  className: string,
}) => {
  if (props.food.length === 0) {
    return null;
  }

  return (
    <Tooltip
      position="bottom-end"
      content={
        <Box>
          <Icon name={props.icon} />  {props.name}
          <Divider />
          <Box>
            {props.food
              .reduce((names, food) => {
                const foodName = FOOD_NAMES[food];
                return foodName ? names.concat(foodName) : names;
              }, []).join(", ")}
          </Box>
        </Box>
      }>
      <Stack ml={2}>
        {props.food.map(food => {
          return FOOD_ICONS[food]
            && (
              <Stack.Item>
                <Icon
                  className={props.className}
                  size={1.4}
                  key={food}
                  name={FOOD_ICONS[food]}
                />
              </Stack.Item>
            );
        })}
      </Stack>
    </Tooltip>
  );
};

const Diet = (props: {
  likedFood: Food[],
  dislikedFood: Food[],
  toxicFood: Food[],
}) => {
  return (
    <Stack>
      <Stack.Item>
        <FoodList
          food={props.likedFood}
          icon="heart"
          name="Liked food"
          className="color-pink"
        />
      </Stack.Item>

      <Stack.Item>
        <FoodList
          food={props.dislikedFood.filter(notIn(IGNORE_UNLESS_LIKED))}
          icon="thumbs-down"
          name="Disliked food"
          className="color-red"
        />
      </Stack.Item>

      <Stack.Item>
        <FoodList
          food={props.toxicFood.filter(notIn(IGNORE_UNLESS_LIKED))}
          icon="biohazard"
          name="Toxic food"
          className="color-olive"
        />
      </Stack.Item>
    </Stack>
  );
};

const SpeciesFeature = (props: {
  className: string,
  feature: Feature,
}) => {
  const { className, feature } = props;

  return (
    <Tooltip position="bottom-end" content={
      <Box>
        <Box as="b">{feature.name}</Box>
        <Divider />
        <Box>{feature.description}</Box>
      </Box>
    }>
      <Box class={className} width="32px" height="32px">
        <Icon
          name={feature.icon}
          size={1.5}
          ml={0}
          mt={1}
          style={{
            "text-align": "center",
            height: "100%",
            width: "100%",
          }}
        />
      </Box>
    </Tooltip>
  );
};

const SpeciesFeatures = (props: {
  features: Species["features"],
}) => {
  const { good, neutral, bad } = props.features;

  return (
    <Stack fill justify="space-between">
      <Stack.Item>
        <Stack>
          {good.map(feature => {
            return (
              <Stack.Item key={feature.name}>
                <SpeciesFeature
                  className="color-bg-green"
                  feature={feature} />
              </Stack.Item>
            );
          })}
        </Stack>
      </Stack.Item>

      <Stack grow>
        {neutral.map(feature => {
          return (
            <Stack.Item key={feature.name}>
              <SpeciesFeature
                className="color-bg-grey"
                feature={feature} />
            </Stack.Item>
          );
        })}
      </Stack>

      <Stack>
        {bad.map(feature => {
          return (
            <Stack.Item key={feature.name}>
              <SpeciesFeature
                className="color-bg-red"
                feature={feature} />
            </Stack.Item>
          );
        })}
      </Stack>
    </Stack>
  );
};

const SpeciesPageInner = (props: {
  handleClose: () => void,
  species: ServerData["species"],
}, context) => {
  const { act, data } = useBackend<PreferencesMenuData>(context);
  const setSpecies = createSetPreference(act, "species");

  let species: [string, Species & ServerSpeciesData][]
    = Object.entries(props.species)
      .map(([species, serverData]) => {
        return [
          species,
          {
            ...serverData,
            ...(requireSpecies.keys().indexOf(`./${species}`) === -1
              ? fallbackSpecies
              : requireSpecies(`./${species}`).default) as Species,
          },
        ];
      });

  // Humans are always the top of the list
  const humanIndex = species.findIndex(([species]) => species === "human");
  const swapWith = species[0];
  species[0] = species[humanIndex];
  species[humanIndex] = swapWith;

  const currentSpecies = species.filter(([speciesKey]) => {
    return speciesKey === data.character_preferences.misc.species;
  })[0][1];

  const { lore } = currentSpecies;

  return (
    <Stack vertical fill>
      <Stack.Item>
        <Button icon="arrow-left" onClick={props.handleClose}>
          Go back
        </Button>
      </Stack.Item>

      <Stack.Item grow>
        <Stack fill>
          <Stack.Item>
            <Box height="calc(100vh - 170px)" overflowY="auto" pr={3}>
              {species.map(([speciesKey, species]) => {
                return (
                  <Button
                    key={speciesKey}
                    onClick={() => setSpecies(speciesKey)}
                    selected={
                      data.character_preferences.misc.species === speciesKey
                    }
                    tooltip={species.name}
                    style={{
                      display: "block",
                      height: "64px",
                      width: "64px",
                    }}
                  >
                    <Box
                      className={classes([
                        "species64x64",
                        species.icon,
                      ])}
                      ml={-1}
                    />
                  </Button>
                );
              })}
            </Box>
          </Stack.Item>

          <Stack.Item grow>
            <Box fill>
              <Box>
                <Stack fill>
                  <Stack.Item width="70%">
                    <Section title={currentSpecies.name} buttons={
                      // Species with no hunger don't have diets
                      currentSpecies.liked_food
                  && (<Diet
                    likedFood={currentSpecies.liked_food}
                    dislikedFood={currentSpecies.disliked_food}
                    toxicFood={currentSpecies.toxic_food}
                  />)
                    }>

                        {currentSpecies.description}

                        <SpeciesFeatures features={currentSpecies.features} />

                  </Stack.Item>

                  <Stack.Item width="30%">
                    <CharacterPreview
                      id={data.character_preview_view}
                      height="100%"
                    />
                  </Stack.Item>
                </Stack>
              </Box>

              {lore && (
                <Box mt={1}>

                      {lore.map((text, index) => (
                        <Box key={index} maxWidth="100%">
                          {text}
                          {index !== lore.length - 1
                            && (<>

</>)}
                        </Box>
                      ))}

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

export const SpeciesPage = (props: {
  closeSpecies: () => void,
}) => {
  return (
    <ServerPreferencesFetcher
      render={serverData => {
        if (serverData) {
          return (<SpeciesPageInner
            handleClose={props.closeSpecies}
            species={serverData.species}
          />);
        } else {
          return <Box>Loading species...</Box>;
        }
      }}
    />
  );
};


code:
import { Component, createRef, InfernoNode, RefObject } from "inferno";
import { Button, Section, Stack } from "../../components";
import { FlexProps } from "../../components/Flex";

type TabbedMenuProps = {
  categoryEntries: [string, InfernoNode][],
  contentProps?: FlexProps,
};

export class TabbedMenu extends Component<TabbedMenuProps> {
  categoryRefs: Record<string, RefObject<HTMLDivElement>> = {};
  sectionRef: RefObject<HTMLDivElement> = createRef();

  getCategoryRef(category: string): RefObject<HTMLDivElement> {
    if (!this.categoryRefs[category]) {
      this.categoryRefs[category] = createRef();
    }

    return this.categoryRefs[category];
  }

  render() {
    return (
      <Stack vertical fill>
        <Stack.Item>
          <Stack fill px={5}>
            {this.props.categoryEntries.map(([category]) => {
              return (
                <Stack.Item key={category} grow basis="content">
                  <Button
                    align="center"
                    fontSize="1.2em"
                    fluid
                    onClick={() => {
                      const offsetTop = this.categoryRefs[category]
                        .current?.offsetTop;

                      if (offsetTop === undefined) {
                        return;
                      }

                      const currentSection = this.sectionRef.current;

                      if (!currentSection) {
                        return;
                      }

                      currentSection.scrollTop = offsetTop;
                    }}
                  >
                    {category}
                  </Button>
                </Stack.Item>
              );
            })}
          </Stack>
        </Stack.Item>

        <Stack.Item
          grow
          innerRef={this.sectionRef}
          position="relative"
          overflowY="scroll"
          {...{
            ...this.props.contentProps,

            // Otherwise, TypeScript complains about invalid prop
            className: undefined,
          }}
        >
          <Stack vertical fill px={2}>
            {this.props.categoryEntries.map(
              ([category, children]) => {
                return (
                  <Stack.Item
                    key={category}
                    innerRef={this.getCategoryRef(category)}
                  >
                    <Section
                      fill
                      title={category}
                    >
                      {children}
                    </Section>
                  </Stack.Item>
                );
              }
            )}
          </Stack>
        </Stack.Item>
      </Stack>
    );
  }
}


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Abductor: Antagonist = {
  key: "abductor",
  name: "Abductor",
  description: [
    multiline`
      Abductors are technologically advanced alien society set on cataloging
      all species in the system. Unfortunately for their subjects their methods
      are quite invasive.
    `,

    multiline`
      You and a partner will become the abductor scientist and agent duo.
      As an agent, abduct unassuming victims and bring them back to your UFO.
      As a scientist, scout out victims for your agent, keep them safe, and
      operate on whoever they bring back.
    `,
  ],
  category: Category.Midround,
};

export default Abductor;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const BLOB_MECHANICAL_DESCRIPTION = multiline`
  The blob infests the station and destroys everything in its path, including
  hull, fixtures, and creatures. Spread your mass, collect resources, and
  consume the entire station. Make sure to prepare your defenses, because the
  crew will be alerted to your presence!
`;

const Blob: Antagonist = {
  key: "blob",
  name: "Blob",
  description: [
    BLOB_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
};

export default Blob;


code:
import { Antagonist, Category } from "../base";
import { BLOB_MECHANICAL_DESCRIPTION } from "./blob";
import { multiline } from "common/string";

const BlobInfection: Antagonist = {
  key: "blobinfection",
  name: "Blob Infection",
  description: [
    multiline`
      At any point in the middle of the shift, be strucken with an infection
      that will turn you into the terrifying blob.
    `,
    BLOB_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
};

export default BlobInfection;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const BloodBrother: Antagonist = {
  key: "bloodbrother",
  name: "Blood Brother",
  description: [
    multiline`
      Team up with other crew members as blood brothers to combine the strengths
      of your departments, break each other out of prison, and overwhelm the
      station.
    `,
  ],
  category: Category.Roundstart,
};

export default BloodBrother;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Changeling: Antagonist = {
  key: "changeling",
  name: "Changeling",
  description: [
    multiline`
      A highly intelligent alien predator that is capable of altering their
      shape to flawlessly resemble a human.
    `,

    multiline`
      Transform yourself or others into different identities, and buy from an
      arsenal of biological weaponry with the DNA you collect.
    `,
  ],
  category: Category.Roundstart,
};

export default Changeling;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";
import { OPERATIVE_MECHANICAL_DESCRIPTION } from "./operative";

const ClownOperative: Antagonist = {
  key: "clownoperative",
  name: "Clown Operative",
  description: [
    multiline`
      Honk! You have been chosen, for better or worse to join the Syndicate
      Clown Operative strike team. Your mission, whether or not you choose
      to tickle it, is to honk Nanotrasen's most advanced research facility!
      That's right, you're going to Clown Station 13.
    `,

    OPERATIVE_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
};

export default ClownOperative;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Cultist: Antagonist = {
  key: "cultist",
  name: "Cultist",
  description: [
    multiline`
      The Geometer of Blood, Nar-Sie, has sent a number of her followers to
      Space Station 13. As a cultist, you have an abundance of cult magics at
      your disposal, something for all situations. You must work with your
      brethren to summon an avatar of your eldritch goddess!
    `,

    multiline`
      Armed with blood magic, convert crew members to the Blood Cult, sacrifice
      those who get in the way, and summon Nar-Sie.
    `,
  ],
  category: Category.Roundstart,
};

export default Cultist;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";
import { GANGSTER_MECHANICAL_DESCRIPTION } from "./gangster";

const FamilyHeadAspirant: Antagonist = {
  key: "familyheadaspirant",
  name: "Family Head Aspirant",
  description: [
    multiline`
      A form of family leader that can activate at any point in the middle
      of the shift.
    `,
    GANGSTER_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
};

export default FamilyHeadAspirant;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const GANGSTER_MECHANICAL_DESCRIPTION
   = multiline`
      Convince people to join your family, wear your uniform, tag turf
      for the family, and accomplish your family's goals.
   `;

const Gangster: Antagonist = {
  key: "gangster",
  name: "Family Leader",
  description: [
    GANGSTER_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
};

export default Gangster;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const REVOLUTIONARY_MECHANICAL_DESCRIPTION
   = multiline`
      Armed with a flash, convert as many people to the revolution as you can.
      Kill or exile all heads of staff on the station.
   `;

const HeadRevolutionary: Antagonist = {
  key: "headrevolutionary",
  name: "Head Revolutionary",
  description: [
    "VIVA LA REVOLUTION!",
    REVOLUTIONARY_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
};

export default HeadRevolutionary;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const HERETIC_MECHANICAL_DESCRIPTION
   = multiline`
      Find hidden influences and sacrifice crew members to gain magical
      powers and ascend as one of several paths.
   `;

const Heretic: Antagonist = {
  key: "heretic",
  name: "Heretic",
  description: [
    multiline`
      Forgotten, devoured, gutted. Humanity has forgotten the eldritch forces
      of decay, but the mansus veil has weakened. We will make them taste fear
      again...
    `,
    HERETIC_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
};

export default Heretic;


code:
import { Antagonist, Category } from "../base";
import { HERETIC_MECHANICAL_DESCRIPTION } from "./heretic";

const HereticSmuggler: Antagonist = {
  key: "hereticsmuggler",
  name: "Heretic Smuggler",
  description: [
    "A form of heretic that can activate when joining an ongoing shift.",
    HERETIC_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Latejoin,
};

export default HereticSmuggler;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";
import { OPERATIVE_MECHANICAL_DESCRIPTION } from "./operative";

const LoneOperative: Antagonist = {
  key: "loneoperative",
  name: "Lone Operative",
  description: [
    multiline`
      A solo nuclear operative that has a higher chance of spawning the longer
      the nuclear authentication disk stays in one place.
    `,

    OPERATIVE_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
};

export default LoneOperative;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const MALF_AI_MECHANICAL_DESCRIPTION
  = multiline`
    With a law zero to complete your objectives at all costs, combine your
    omnipotence and malfunction modules to wreak havoc across the station.
    Go delta to destroy the station and all those who opposed you.
  `;

const MalfAI: Antagonist = {
  key: "malfai",
  name: "Malfunctioning AI",
  description: [
    MALF_AI_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
};

export default MalfAI;


code:
import { multiline } from "common/string";
import { Antagonist, Category } from "../base";
import { MALF_AI_MECHANICAL_DESCRIPTION } from "./malfai";

const MalfAIMidround: Antagonist = {
  key: "malfaimidround",
  name: "Value Drifted AI",
  description: [
    multiline`
      A form of malfunctioning AI that is given to existing AIs in the middle
      of the shift.
    `,
    MALF_AI_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
};

export default MalfAIMidround;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Monkey: Antagonist = {
  key: "monkey",
  name: "Monkey",
  description: [
    multiline`
      When the round starts, become infected with Jungle Fever, a disease which
      turns its victim into a monkey. These monkeys can attack humans and give
      them the deadly virus. Turn the entire crew into monkeys!
    `,
  ],
  category: Category.Roundstart,
};

export default Monkey;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Nightmare: Antagonist = {
  key: "nightmare",
  name: "Nightmare",
  description: [
    multiline`
      Use your light eater to break sources of light to survive and thrive.
      Jaunt through the darkness and seek your prey with night vision.
    `,
  ],
  category: Category.Midround,
};

export default Nightmare;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Obsessed: Antagonist = {
  key: "obsessed",
  name: "Obsessed",
  description: [
    multiline`
    You're obsessed with someone! Your obsession may begin to notice their
    personal items are stolen and their coworkers have gone missing,
    but will they realize they are your next victim in time?
    `,
  ],
  category: Category.Midround,
};

export default Obsessed;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const OPERATIVE_MECHANICAL_DESCRIPTION = multiline`
  Retrieve the nuclear authentication disk, use it to activate the nuclear
  fission explosive, and destroy the station.
`;

const Operative: Antagonist = {
  key: "operative",
  name: "Nuclear Operative",
  description: [
    multiline`
      Congratulations, agent. You have been chosen to join the Syndicate
      Nuclear Operative strike team. Your mission, whether or not you choose
      to accept it, is to destroy Nanotrasen's most advanced research facility!
      That's right, you're going to Space Station 13.
    `,

    OPERATIVE_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
};

export default Operative;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";
import { OPERATIVE_MECHANICAL_DESCRIPTION } from "./operative";

const OperativeMidround: Antagonist = {
  key: "operativemidround",
  name: "Nuclear Assailant",
  description: [
    multiline`
      A form of nuclear operative that is offered to ghosts in the middle
      of the shift.
    `,
    OPERATIVE_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
};

export default OperativeMidround;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";
import { REVOLUTIONARY_MECHANICAL_DESCRIPTION } from "./headrevolutionary";

const Provocateur: Antagonist = {
  key: "provocateur",
  name: "Provocateur",
  description: [
    multiline`
      A form of head revolutionary that can activate when joining an ongoing
      shift.
    `,

    REVOLUTIONARY_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Latejoin,
};

export default Provocateur;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Revenant: Antagonist = {
  key: "revenant",
  name: "Revenant",
  description: [
    multiline`
      Become the mysterious revenant. Break windows, overload lights, and eat
      the crew's life force, all while talking to your old community of
      disgruntled ghosts.
    `,
  ],
  category: Category.Midround,
};

export default Revenant;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const SentientDisease: Antagonist = {
  key: "sentientdisease",
  name: "Sentient Disease",
  description: [
    multiline`
      Mutate and spread yourself and infect as much of the crew as possible
      with a deadly plague of your own creation.
    `,
  ],
  category: Category.Midround,
};

export default SentientDisease;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const SpaceDragon: Antagonist = {
  key: "spacedragon",
  name: "Space Dragon",
  description: [
    multiline`
      Become a ferocious space dragon. Breathe fire, summon an army of space
      carps, crush walls, and terrorize the station.
    `,
  ],
  category: Category.Midround,
};

export default SpaceDragon;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const SpaceNinja: Antagonist = {
  key: "spaceninja",
  name: "Space Ninja",
  description: [
    multiline`
      The Spider Clan practice a sort of augmentation of human flesh in order to
      achieve a more perfect state of being and follow Postmodern Space Bushido.
    `,

    multiline`
      Become a conniving space ninja, equipped with a katana, gloves to hack
      into airlocks and APCs, a suit to make you go near-invisible,
      as well as a variety of abilities in your kit. Hack into arrest consoles
      to mark everyone as arrest, and even hack into communication consoles to
      summon more threats to cause chaos on the station!
    `,
  ],
  category: Category.Midround,
};

export default SpaceNinja;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Swarmer: Antagonist = {
  key: "swarmer",
  name: "Swarmer",
  description: [
    multiline`
      A swarmer is a small robot that replicates itself autonomously with
      nearby given materials and prepare structures that they come
      across for the following invasion force.
    `,

    multiline`
      Consume machines, structures, walls, anything to get materials. Replicate
      as many swarmers as you can to repeat the process.
    `,
  ],
  category: Category.Midround,
};

export default Swarmer;


code:
import { Antagonist, Category } from "../base";
import { TRAITOR_MECHANICAL_DESCRIPTION } from "./traitor";

const SyndicateInfiltrator: Antagonist = {
  key: "syndicateinfiltrator",
  name: "Syndicate Infiltrator",
  description: [
    "A form of traitor that can activate when joining an ongoing shift.",
    TRAITOR_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Latejoin,
  priority: -1,
};

export default SyndicateInfiltrator;


code:
import { Antagonist, Category } from "../base";
import { TRAITOR_MECHANICAL_DESCRIPTION } from "./traitor";
import { multiline } from "common/string";

const SyndicateSleeperAgent: Antagonist = {
  key: "syndicatesleeperagent",
  name: "Syndicate Sleeper Agent",
  description: [
    multiline`
      A form of traitor that can activate at any point in the middle
      of the shift.
    `,
    TRAITOR_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
  priority: -1,
};

export default SyndicateSleeperAgent;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const TRAITOR_MECHANICAL_DESCRIPTION
   = multiline`
      Start with a set of sinister objectives and an uplink to purchase
      items to get the job done.
   `;

const Traitor: Antagonist = {
  key: "traitor",
  name: "Traitor",
  description: [
    multiline`
      An unpaid debt. A score to be settled. Maybe you were just in the wrong
      place at the wrong time. Whatever the reasons, you were selected to
      infiltrate Space Station 13.
    `,
    TRAITOR_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
  priority: -1,
};

export default Traitor;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

export const WIZARD_MECHANICAL_DESCRIPTION
  = multiline`
      Choose between a variety of powerful spells in order to cause chaos
      among Space Station 13.
    `;

const Wizard: Antagonist = {
  key: "wizard",
  name: "Wizard",
  description: [
    `"GREETINGS. WE'RE THE WIZARDS OF THE WIZARD'S FEDERATION."`,
    WIZARD_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Roundstart,
};

export default Wizard;


code:
import { Antagonist, Category } from "../base";
import { WIZARD_MECHANICAL_DESCRIPTION } from "./wizard";

const WizardMidround: Antagonist = {
  key: "wizardmidround",
  name: "Wizard (Midround)",
  description: [
    "A form of wizard that is offered to ghosts in the middle of the shift.",
    WIZARD_MECHANICAL_DESCRIPTION,
  ],
  category: Category.Midround,
};

export default WizardMidround;


code:
import { Antagonist, Category } from "../base";
import { multiline } from "common/string";

const Xenomorph: Antagonist = {
  key: "xenomorph",
  name: "Xenomorph",
  description: [
    multiline`
      Become the extraterrestrial xenomorph. Start as a larva, and progress
      your way up the caste, including even the Queen!
    `,
  ],
  category: Category.Midround,
};

export default Xenomorph;


code:
/**
 * This folder represents the antagonists you can choose in the preferences
 * menu.
 *
 * Every file in this folder represents one antagonist.
 *
 * For example "Syndicate Sleeper Agent" -> syndicatesleeperagent.ts
 *
 * "Antagonist" in this context actually means ruleset.
 * This is an important distinction--it means that players can choose to be
 * a roundstart traitor, but not a latejoin traitor.
 *
 * Icons are generated from the antag datums themselves, provided by the
 * `antag_datum` variable on the /datum/dynamic_ruleset.
 *
 * The icon used is whatever the return value of get_preview_icon() is.
 * Most antagonists, unless they want an especially cool effect, can simply
 * set preview_outfit to some typepath representing their character.
 */

export type Antagonist = {
  // the antag_flag, made lowercase, and with non-alphanumerics removed.
  key: string;

  name: string;
  description: string[];
  category: Category;
  priority?: number;
};

export enum Category {
  Roundstart,
  Midround,
  Latejoin,
}


code:
import { BooleanLike } from "common/react";
import { sendAct } from "../../backend";
import { Genital } from "./preferences/features/character_preferences/genitals";
import { Gender } from "./preferences/gender";

export enum Food {
  Alcohol = "ALCOHOL",
  Breakfast = "BREAKFAST",
  Cloth = "CLOTH",
  Dairy = "DAIRY",
  Fried = "FRIED",
  Fruit = "FRUIT",
  Grain = "GRAIN",
  Gross = "GROSS",
  Junkfood = "JUNKFOOD",
  Meat = "MEAT",
  Nuts = "NUTS",
  Pineapple = "PINEAPPLE",
  Raw = "RAW",
  Seafood = "SEAFOOD",
  Sugar = "SUGAR",
  Toxic = "TOXIC",
  Vegetables = "VEGETABLES",
  Sewage = "SEWAGE",
}

export enum JobPriority {
  Low = 1,
  Medium = 2,
  High = 3,
}

export type Name = {
  can_randomize: BooleanLike;
  explanation: string;
  group: string;
};

export type ServerSpeciesData = {
  name: string;
  icon: string;

  use_skintones: BooleanLike;
  sexes: BooleanLike;

  enabled_features: string[];

  liked_food: Food[];
  disliked_food: Food[];
  toxic_food: Food[];
};

export type Quirk = {
  description: string;
  icon: string;
  name: string;
  value: number;
};

export type QuirkInfo = {
  max_positive_quirks: number;
  quirk_info: Record<string, Quirk>;
  quirk_blacklist: string[][];
};

export type Language = {
  name: string;
  description: string;
  understand_type: number;
  icon: string;
  value_understand: number;
  value_speak: number;
  cant_learn_understand: boolean;
  cant_learn_speak: boolean;
  cant_forget: string;
};

export type Marking = {
  name: string;
  color: string;
  marking_index: string;
};

export type MarkingZone = {
  body_zone: string;
  name: string;
  markings_choices: string[];
  markings: Marking[];
  can_add_markings: boolean;
};

export type AugmentItem = {
  category: string;
  slot: string;
  name: string;
  description: string;
  can_use_styles: boolean;
  value: number;
  cant_buy: string;
};

export type AugmentSlot = {
  name: string;
  items: AugmentItem[];
};

export type AugmentCategory = {
  name: string;
  slots: AugmentSlot[];
};

export type SelectedAugmentSlot = {
  name: string;
  item: AugmentItem;
};

export type SelectedAugmentStyle = {
  name: string;
  style: string;
};

export type Birthsign = {
  name: string;
  description: string;
  icon: string;
  patron_saint: string;
  favored_profession: string;
  value: number;
  cant_buy: string;
};

export enum RandomSetting {
  AntagOnly = 1,
  Disabled = 2,
  Enabled = 3,
}

export enum JoblessRole {
  BeOverflow = 1,
  BeRandomJob = 2,
  ReturnToLobby = 3,
}

export enum GamePreferencesSelectedPage {
  Settings,
  Keybindings,
}

export const createSetPreference = (
  act: typeof sendAct,
  preference: string
) => (value: unknown) => {
  act("set_preference", {
    preference,
    value,
  });
};

export enum Window {
  Character = 0,
  Game = 1,
  Keybindings = 2,
}

export type PreferencesMenuData = {
  character_preview_type: string,
  character_preview_view: string;
  character_profiles: (string | null)[];

  character_preferences: {
    clothing: Record<string, string>;
    features: Record<string, string>;
    game_preferences: Record<string, unknown>;
    non_contextual: {
      random_body: RandomSetting,
      [otherKey: string]: unknown;
    };
    secondary_features: Record<string, unknown>;
    supplemental_features: Record<string, unknown>;

    names: Record<string, string>;

    misc: {
      gender: Gender;
      genitals: Genital;
      joblessrole: JoblessRole;
      species: string;
    };

    randomization: Record<string, RandomSetting>;
  };

  content_unlocked: BooleanLike,
  donator_rank: String,

  job_bans?: string[];
  job_days_left?: Record<string, number>;
  job_required_experience?: Record<string, {
    experience_type: string,
    required_playtime: number,
  }>;
  job_preferences: Record<string, JobPriority>;

  keybindings: Record<string, string[]>;
  overflow_role: string;

  selected_quirks: string[];
  selected_languages: Language[];
  unselected_languages: Language[];
  maximum_customization_points: number;
  language_balance: number;
  marking_parts: MarkingZone[];
  maximum_markings_per_limb: number;
  body_marking_sets: string[];
  selected_augments: SelectedAugmentSlot[];
  selected_augments_styles: SelectedAugmentStyle[];
  unselected_augments: AugmentCategory[];
  unselected_augments_styles: string[];
  augment_balance: number;
  selected_birthsign: Birthsign;
  unselected_birthsigns: Birthsign[];
  birthsign_balance: number;

  antag_bans?: string[];
  antag_days_left?: Record<string, number>;
  selected_antags: string[];

  active_slot: number;
  name_to_use: string;

  window: Window;
};

export type ServerData = {
  names: {
    types: Record<string, Name>;
  };
  quirks: QuirkInfo,
  random: {
    randomizable: string[];
  };
  species: Record<string, ServerSpeciesData>;
  [otheyKey: string]: unknown;
};


code:
import { exhaustiveCheck } from "common/exhaustive";
import { useBackend } from "../../backend";
import { GamePreferencesSelectedPage, PreferencesMenuData, Window } from "./data";
import { CharacterPreferenceWindow } from "./CharacterPreferenceWindow";
import { GamePreferenceWindow } from "./GamePreferenceWindow";

export const PreferencesMenu = (props, context) => {
  const { data } = useBackend<PreferencesMenuData>(context);

  const window = data.window;

  switch (window) {
    case Window.Character:
      return <CharacterPreferenceWindow />;
    case Window.Game:
      return <GamePreferenceWindow />;
    case Window.Keybindings:
      return (<GamePreferenceWindow
        startingPage={GamePreferencesSelectedPage.Keybindings}
      />);
    default:
      exhaustiveCheck(window);
  }
};


code:
import { Department } from "./departments";

export type Job = {
  name: string;
  description: string;
  department: Department;
};


code:
export type Department = {
  head?: string;
};

export const Assistant: Department = {
  // "Assistant" is not the head of its own department, as otherwise
  // it would show as large and bold.
};

export const Captain: Department = {
  head: "Captain",
};

export const Cargo: Department = {
  head: "Quartermaster",
};

export const Engineering: Department = {
  head: "Chief Engineer",
};

export const Medical: Department = {
  head: "Chief Medical Officer",
};

export const Security: Department = {
  head: "Head of Security",
};

export const Service: Department = {
  head: "Head of Personnel",
};

export const Science: Department = {
  head: "Research Director",
};

export const Silicon: Department = {
  head: "AI",
};


code:
import { Job } from "../base";
import { Silicon } from "../departments";

const AI: Job = {
  name: "AI",
  description: "Assist the crew, follow your laws, coordinate your cyborgs.",
  department: Silicon,
};

export default AI;


code:
import { Job } from "../base";
import { Assistant as DepartmentAssistant } from "../departments";

const Assistant: Job = {
  name: "Assistant",
  description: "Get your space legs, assist people, ask the HoP to \
    give you a job.",
  department: DepartmentAssistant,
};

export default Assistant;


code:
import { Job } from "../base";
import { Engineering } from "../departments";

const AtmosphericTechnician: Job = {
  name: "Atmospheric Technician",
  description: "Ensure the air is breathable on the station, fill oxygen \
    tanks, fight fires, purify the air.",
  department: Engineering,
};

export default AtmosphericTechnician;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Bartender: Job = {
  name: "Bartender",
  description: "Serve booze, mix drinks, keep the crew drunk.",
  department: Service,
};

export default Bartender;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Botanist: Job = {
  name: "Botanist",
  description: "Grow plants for the cook, for medicine, and for recreation.",
  department: Service,
};

export default Botanist;


code:
import { Job } from "../base";
import { Captain as DepartmentCaptain } from "../departments";

const Captain: Job = {
  name: "Captain",
  description: "Be responsible for the station, manage your Heads of Staff, \
    keep the crew alive, be prepared to do anything and everything or die \
    horribly trying.",
  department: DepartmentCaptain,
};

export default Captain;


code:
import { Job } from "../base";
import { Cargo } from "../departments";

const CargoTechnician: Job = {
  name: "Cargo Technician",
  description: "Distribute supplies to the departments that ordered them, \
    collect empty crates, load and unload the supply shuttle, \
    ship bounty cubes.",
  department: Cargo,
};

export default CargoTechnician;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Chaplain: Job = {
  name: "Chaplain",
  description: "Hold services and funerals, cremate people, preach your \
    religion, protect the crew against cults.",
  department: Service,
};

export default Chaplain;


code:
import { Job } from "../base";
import { Medical } from "../departments";

const Chemist: Job = {
  name: "Chemist",
  description: "Supply the doctors with chemicals, make medicine, as well as \
    less likable substances in the comfort of a fully reinforced room.",
  department: Medical,
};

export default Chemist;


code:
import { Job } from "../base";
import { Engineering } from "../departments";

const ChiefEngineer: Job = {
  name: "Chief Engineer",
  description: "Coordinate engineering, ensure equipment doesn't get stolen, \
    make sure the Supermatter doesn't blow up, maintain telecommunications.",
  department: Engineering,
};

export default ChiefEngineer;


code:
import { Job } from "../base";
import { Medical } from "../departments";

const ChiefMedicalOfficer: Job = {
  name: "Chief Medical Officer",
  description: "Coordinate doctors and other medbay employees, ensure they \
    know how to save lives, check for injuries on the crew monitor.",
  department: Medical,
};

export default ChiefMedicalOfficer;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Clown: Job = {
  name: "Clown",
  description: "Entertain the crew, make bad jokes, go on a holy quest to find \
    bananium, HONK!",
  department: Service,
};

export default Clown;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Cook: Job = {
  name: "Cook",
  description: "Serve food, cook meat, keep the crew fed.",
  department: Service,
};

export default Cook;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Curator: Job = {
  name: "Curator",
  description: "Read and write books and hand them to people, stock \
    bookshelves, report on station news.",
  department: Service,
};

export default Curator;


code:
import { Job } from "../base";
import { Silicon } from "../departments";

const Cyborg: Job = {
  name: "Cyborg",
  description: "Assist the crew, follow your laws, obey your AI.",
  department: Silicon,
};

export default Cyborg;


code:
import { Job } from "../base";
import { Security } from "../departments";

const Detective: Job = {
  name: "Detective",
  description: "Investigate crimes, gather evidence, perform interrogations, \
    look badass, smoke cigarettes.",
  department: Security,
};

export default Detective;


code:
import { Job } from "../base";
import { Science } from "../departments";

const Geneticist: Job = {
  name: "Geneticist",
  description: "Alter genomes, turn monkeys into humans (and vice-versa), make \
    DNA backups so they can get stolen.",
  department: Science,
};

export default Geneticist;


code:
import { Job } from "../base";
import { Service } from "../departments";

const HeadOfPersonnel: Job = {
  name: "Head of Personnel",
  description: "Alter access on ID cards, manage civil and supply departments, \
    protect Ian, run the station when the captain dies.",
  department: Service,
};

export default HeadOfPersonnel;


code:
import { Job } from "../base";
import { Security } from "../departments";

const HeadOfSecurity: Job = {
  name: "Head of Security",
  description: "Coordinate security personnel, ensure they are not corrupt, \
    make sure every department is protected, save the Virologist when he gets \
    mauled by monkeys.",
  department: Security,
};

export default HeadOfSecurity;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Janitor: Job = {
  name: "Janitor",
  description: "Clean up trash and blood. Replace broken lights. \
    Slip people over.",
  department: Service,
};

export default Janitor;


code:
import { Job } from "../base";
import { Security } from "../departments";

const Lawyer: Job = {
  name: "Lawyer",
  description: "Advocate for prisoners, create law-binding contracts, \
    ensure Security is following protocol and Space Law.",
  department: Security,
};

export default Lawyer;


code:
import { Job } from "../base";
import { Medical } from "../departments";

const MedicalDoctor: Job = {
  name: "Medical Doctor",
  description: "Save lives, run around the station looking for victims, \
    scan everyone in sight",
  department: Medical,
};

export default MedicalDoctor;


code:
import { Job } from "../base";
import { Service } from "../departments";

const Mime: Job = {
  name: "Mime",
  description: "...",
  department: Service,
};

export default Mime;


code:
import { Job } from "../base";
import { Medical } from "../departments";

const Paramedic: Job = {
  name: "Paramedic",
  description: "Run around the station looking for patients, respond to \
    emergencies, give patients a roller bed ride to medbay.",
  department: Medical,
};

export default Paramedic;


code:
import { Job } from "../base";
import { Security } from "../departments";

const Prisoner: Job = {
  name: "Prisoner",
  description: "Keep yourself occupied while in permabrig.",
  department: Security,
};

export default Prisoner;


code:
import { Job } from "../base";
import { Medical } from "../departments";

const Psychologist: Job = {
  name: "Psychologist",
  description: "Advocate sanity, self-esteem, and teamwork in a station \
    staffed with headcases.",
  department: Medical,
};

export default Psychologist;


code:
import { Job } from "../base";
import { Cargo } from "../departments";

const Quartermaster: Job = {
  name: "Quartermaster",
  description: "Coordinate cargo technicians and shaft miners, assist with \
    economical purchasing.",
  department: Cargo,
};

export default Quartermaster;


code:
import { Job } from "../base";
import { Science } from "../departments";

const ResearchDirector: Job = {
  name: "Research Director",
  description: "Supervise research efforts, ensure Robotics is in working \
    order, make sure the AI and its Cyborgs aren't rogue, replacing them if \
    they are",
  department: Science,
};

export default ResearchDirector;


code:
import { Job } from "../base";
import { Science } from "../departments";

const Roboticist: Job = {
  name: "Roboticist",
  description: "Build and repair the AI and cyborgs, create mechs.",
  department: Science,
};

export default Roboticist;


code:
import { Job } from "../base";
import { Science } from "../departments";

const Scientist: Job = {
  name: "Scientist",
  description: "Do experiments, perform research, feed the slimes, make bombs.",
  department: Science,
};

export default Scientist;


code:
import { Job } from "../base";
import { Security } from "../departments";

const SecurityOfficer: Job = {
  name: "Security Officer",
  description: "Protect company assets, follow the Standard Operating \
    Procedure, eat donuts",
  department: Security,
};

export default SecurityOfficer;


code:
import { Job } from "../base";
import { Cargo } from "../departments";

const ShaftMiner: Job = {
  name: "Shaft Miner",
  description: "Travel to strange lands. Mine ores. \
    Meet strange creatures. Kill them for their gold.",
  department: Cargo,
};

export default ShaftMiner;


code:
import { Job } from "../base";
import { Engineering } from "../departments";

const StationEngineer: Job = {
  name: "Station Engineer",
  description: "Start the Supermatter, wire the solars, repair station hull \
    and wiring damage.",
  department: Engineering,
};

export default StationEngineer;


code:
import { Job } from "../base";
import { Medical } from "../departments";

const Virologist: Job = {
  name: "Virologist",
  description: "Study the effects of various diseases and synthesize a \
    vaccine for them. Engineer beneficial viruses.",
  department: Medical,
};

export default Virologist;


code:
import { Job } from "../base";
import { Security } from "../departments";

const Warden: Job = {
  name: "Warden",
  description: "Watch over the Brig and Prison Wing, release prisoners when \
    their time is up, issue equipment to security, be a security officer when \
    they all eventually die.",
  department: Security,
};

export default Warden;


code:
import { binaryInsertWith, sortBy } from "common/collections";
import { useLocalState } from "../../backend";
import { Box, Button, FitText, Icon, Input, LabeledList, Modal, Section, Stack, TrackOutsideClicks } from "../../components";
import { Name } from "./data";
import { ServerPreferencesFetcher } from "./ServerPreferencesFetcher";

type NameWithKey = {
  key: string,
  name: Name,
};

const binaryInsertName = binaryInsertWith<NameWithKey>(({ key }) => key);

const sortNameWithKeyEntries = sortBy<[string, NameWithKey[]]>(
  ([key]) => key,
);

export const MultiNameInput = (props: {
  handleClose: () => void,
  handleRandomizeName: (nameType: string) => void,
  handleUpdateName: (nameType: string, value: string) => void,
  names: Record<string, string>,
}, context) => {
  const [currentlyEditingName, setCurrentlyEditingName]
    = useLocalState<string | null>(context, "currentlyEditingName", null);

  return (
    <ServerPreferencesFetcher render={data => {
      if (!data) {
        return null;
      }

      const namesIntoGroups: Record<string, NameWithKey[]> = {};

      for (const [key, name] of Object.entries(data.names.types)) {
        namesIntoGroups[name.group] = binaryInsertName(
          namesIntoGroups[name.group] || [],
          {
            key,
            name,
          }
        );
      }

      return (
        <Modal style={{
          "margin": "0 auto",
          "width": "40%",
        }}>
          <TrackOutsideClicks onOutsideClick={props.handleClose}>
            <Section
              buttons={(
                <Button
                  color="red"
                  onClick={props.handleClose}
                >
                  Close
                </Button>
              )}
              title="Alternate names"
            >
              <LabeledList>
                {sortNameWithKeyEntries(Object.entries(namesIntoGroups))
                  .map(([_, names], index, collection) => (
                    <>
                      {names.map(({ key, name }) => {
                        let content;

                        if (currentlyEditingName === key) {
                          const updateName = (event, value) => {
                            props.handleUpdateName(
                              key,
                              value,
                            );

                            setCurrentlyEditingName(null);
                          };

                          content = (
                            <Input
                              autoSelect
                              onEnter={updateName}
                              onChange={updateName}
                              onEscape={() => {
                                setCurrentlyEditingName(null);
                              }}
                              value={props.names[key]}
                            />
                          );
                        } else {
                          content = (
                            <Button
                              width="100%"
                              onClick={(event) => {
                                setCurrentlyEditingName(key);
                                event.cancelBubble = true;
                                event.stopPropagation();
                              }}
                            >
                              <FitText maxFontSize={12} maxWidth={130}>
                                {props.names[key]}
                              </FitText>
                            </Button>
                          );
                        }

                        return (
                          <LabeledList.Item
                            key={key}
                            label={name.explanation}
                          >
                            <Stack fill>
                              <Stack.Item grow>
                                {content}
                              </Stack.Item>

                              {!!name.can_randomize && (
                                <Stack.Item>
                                  <Button
                                    icon="dice"
                                    tooltip="Randomize"
                                    tooltipPosition="right"
                                    onClick={() => {
                                      props.handleRandomizeName(key);
                                    }}
                                  />
                                </Stack.Item>
                              )}
                            </Stack>
                          </LabeledList.Item>
                        );
                      })}

                      {(index !== collection.length - 1)
                        && <LabeledList.Divider />}
                    </>
                  ))}
              </LabeledList>
            </Section>
          </TrackOutsideClicks>
        </Modal>
      );
    }} />
  );
};

export const NameInput = (props: {
  handleUpdateName: (name: string) => void,
  name: string,
  openMultiNameInput: () => void,
}, context) => {
  const [lastNameBeforeEdit, setLastNameBeforeEdit]
    = useLocalState<string | null>(context, "lastNameBeforeEdit", null);
  const editing = lastNameBeforeEdit === props.name;

  const updateName = (e, value) => {
    setLastNameBeforeEdit(null);
    props.handleUpdateName(value);
  };

  return (
    <Button captureKeys={!editing} onClick={() => {
      setLastNameBeforeEdit(props.name);
    }} textAlign="center" width="100%" height="28px">
      <Stack align="center" fill>
        <Stack.Item>
          <Icon style={{
            "color": "rgba(255, 255, 255, 0.5)",
            "font-size": "17px",
          }} name="edit" />
        </Stack.Item>

        <Stack.Item grow position="relative">
          {editing && (
            <Input
              autoSelect
              onEnter={updateName}
              onChange={updateName}
              onEscape={() => {
                setLastNameBeforeEdit(null);
              }}
              value={props.name}
            />
          ) || (
            <FitText maxFontSize={16} maxWidth={130}>
              {props.name}
            </FitText>
          )}

          <Box style={{
            "border-bottom": "2px dotted rgba(255, 255, 255, 0.8)",
            right: "50%",
            transform: "translateX(50%)",
            position: "absolute",
            width: "90%",
            bottom: "-1px",
          }} />
        </Stack.Item>

        {/* We only know other names when the server tells us */}
        <ServerPreferencesFetcher render={data => data ? (
          <Stack.Item>
            <Button
              as="span"
              tooltip="Alternate Names"
              tooltipPosition="bottom"
              style={{
                background: "rgba(0, 0, 0, 0.7)",
                position: "absolute",
                right: "2px",
                top: "50%",
                transform: "translateY(-50%)",
                width: "2%",
              }}
              onClick={(event) => {
                props.openMultiNameInput();

                // We're a button inside a button.
                // Did you know that's against the W3C standard? :)
                event.cancelBubble = true;
                event.stopPropagation();
              }}
            >
              <Icon name="ellipsis-v" style={{
                "position": "relative",
                "left": "1px",
                "min-width": "0px",
              }} />
            </Button>
          </Stack.Item>
        ) : null} />
      </Stack>
    </Button>
  );
};


code:
import { Department } from "./departments";

export type Job = {
  name: string;
  description: string;
  department: Department;
};


code:
export type Department = {
  head?: string;
};

export const Beggar: Department = {
  // "Beggar" is not the head of its own department, as otherwise
  // it would show as large and bold.
};

export const Nobility: Department = {
  head: "Doge",
};

export const Bourgeouis: Department = {
  head: "Merchant",
};

export const Proletariat: Department = {
};

export const Unpersons: Department = {
  head: "Beggar",
};


code:
import { Job } from "../base";
import { Unpersons } from "../departments";

const Beggar: Job = {
  name: "Beggar",
  description: "Go around being a completely unproductive member of the outpost. \
    Uneducated, and chronic malnutrition has made you weak and frail.",
  department: Unpersons,
};

export default Beggar;


code:
import { Job } from "../base";
import { Bourgeouis } from "../departments";

const Innkeeper: Job = {
  name: "Innkeeper",
  description: "Serve booze, mix drinks, stop petty bar fights and keep the outpost fed \
    - At a fair price, of course.",
  department: Bourgeouis,
};

export default Innkeeper;


code:
import { Job } from "../base";
import { Proletariat } from "../departments";

const Seeder: Job = {
  name: "Seeder",
  description: "Make fun of all the city slickers. \
    Feed and seed your soil. \
    Milk your garloids. \
    It ain't much, but it's honest work.",
  department: Proletariat,
};

export default Seeder;


code:
import { Job } from "../base";
import { Nobility } from "../departments";

const Doge: Job = {
  name: "Doge",
  description: "Command the outpost. Enacting tyrannical laws or being benevolent, \
    it doesn't matter - You are in control.",
  department: Nobility,
};

export default Doge;


code:
import { Job } from "../base";
import { Proletariat } from "../departments";

const Freighter: Job = {
  name: "Freighter",
  description: "Act as the Merchant's work horse, lugging crates around and \
    delivering orders. Get paid a small part of \
    company profits.",
  department: Proletariat,
};

export default Freighter;


code:
import { Job } from "../base";
import { Bourgeouis } from "../departments";

const Pharmacist: Job = {
  name: "Pharmacist",
  description: "Sell the Hippocrite and their underlings chemicals. \
    Give people their legal - Or even illegal - high.",
  department: Bourgeouis,
};

export default Pharmacist;


code:
import { Job } from "../base";
import { Nobility } from "../departments";

const Foreman: Job = {
  name: "Foreman",
  description: "Ensure your pioneers are properly powering and heating the outpost. \
    Try and fail to maintain the integrity of the outpost in the face of unavoidable \
    tragedy.",
  department: Nobility,
};

export default Foreman;


code:
import { Job } from "../base";
import { Nobility } from "../departments";

const Hippocrite: Job = {
  name: "Hippocrite",
  description: "Coordinate your humorist underlings. \
    Try to ensure the outpost is healthy - As long as people pay.",
  department: Nobility,
};

export default Hippocrite;


code:
import { Job } from "../base";
import { Proletariat } from "../departments";

const Jester: Job = {
  name: "Jester",
  description: "Go around the outpost, make people laugh and \
    get beat up by the ordinators for trying to cheer them up.",
  department: Proletariat,
};

export default Jester;


code:
import { Job } from "../base";
import { Nobility } from "../departments";

const Gatekeeper: Job = {
  name: "Gatekeeper",
  description: "Deal with migrants, emmigrants and those who need to go outside for \
  other reasons. If someone gets on your bad side, throw them on the acid pit. \
  If the Doge dies, you're next in line. \
  Glory to ZoomTech.",
  department: Nobility,
};

export default Gatekeeper;


code:
import { Job } from "../base";
import { Nobility } from "../departments";

const Coordinator: Job = {
  name: "Coordinator",
  description: "Coordinate your ordinators and ensure the Doge's will is \
    being enacted. Commit excessive use of force against petty criminals.",
  department: Nobility,
};

export default Coordinator;


code:
import { Job } from "../base";
import { Proletariat } from "../departments";

const Janitor: Job = {
  name: "Janitor",
  description: "Clean up everyone's mess, get made fun of because you \"Do it for free\".",
  department: Proletariat,
};

export default Janitor;


code:
import { Job } from "../base";
import { Bourgeouis } from "../departments";

const Humorist: Job = {
  name: "Humorist",
  description: "Save lives, at a fair price. Hopefully, no one's \
    card gets declined.",
  department: Bourgeouis,
};

export default Humorist;


code:
import { Job } from "../base";
import { Proletariat } from "../departments";

const Sanitar: Job = {
  name: "Sanitar",
  description: "Run around the outpost and save those bleeding out on the spot. \
    Serve the Hippocrite when he asks you to do something outside of the \
    medical bay.",
  department: Proletariat,
};

export default Sanitar;