code:
 #!/usr/bin/python
 # -*- coding: utf-8 -*-

from __future__ import division
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import absolute_import
from builtins import range
from builtins import int
from builtins import chr
from builtins import object
import math
import re

keyStrBase64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
keyStrUriSafe = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$"
baseReverseDic = {};

class Object(object):
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

def getBaseValue(alphabet, character):
    if alphabet not in baseReverseDic:
        baseReverseDic[alphabet] = {}
    for i in range(len(alphabet)):
        baseReverseDic[alphabet][alphabet[i]] = i
    return baseReverseDic[alphabet][character]

def _compress(uncompressed, bitsPerChar, getCharFromInt):
    if (uncompressed is None):
        return ""

    context_dictionary = {}
    context_dictionaryToCreate= {}
    context_c = ""
    context_wc = ""
    context_w = ""
    context_enlargeIn = 2 # Compensate for the first entry which should not count
    context_dictSize = 3
    context_numBits = 2
    context_data = []
    context_data_val = 0
    context_data_position = 0

    for ii in range(len(uncompressed)):
        if isinstance(uncompressed, (bytes)):
            context_c = chr(uncompressed[ii])
        else:
            context_c = uncompressed[ii]
        if context_c not in context_dictionary:
            context_dictionary[context_c] = context_dictSize
            context_dictSize += 1
            context_dictionaryToCreate[context_c] = True

        context_wc = context_w + context_c
        if context_wc in context_dictionary:
            context_w = context_wc
        else:
            if context_w in context_dictionaryToCreate:
                if ord(context_w[0]) < 256:
                    for i in range(context_numBits):
                        context_data_val = (context_data_val << 1)
                        if context_data_position == bitsPerChar-1:
                            context_data_position = 0
                            context_data.append(getCharFromInt(context_data_val))
                            context_data_val = 0
                        else:
                            context_data_position += 1
                    value = ord(context_w[0])
                    for i in range(8):
                        context_data_val = (context_data_val << 1) | (value & 1)
                        if context_data_position == bitsPerChar - 1:
                            context_data_position = 0
                            context_data.append(getCharFromInt(context_data_val))
                            context_data_val = 0
                        else:
                            context_data_position += 1
                        value = value >> 1

                else:
                    value = 1
                    for i in range(context_numBits):
                        context_data_val = (context_data_val << 1) | value
                        if context_data_position == bitsPerChar - 1:
                            context_data_position = 0
                            context_data.append(getCharFromInt(context_data_val))
                            context_data_val = 0
                        else:
                            context_data_position += 1
                        value = 0
                    value = ord(context_w[0])
                    for i in range(16):
                        context_data_val = (context_data_val << 1) | (value & 1)
                        if context_data_position == bitsPerChar - 1:
                            context_data_position = 0
                            context_data.append(getCharFromInt(context_data_val))
                            context_data_val = 0
                        else:
                            context_data_position += 1
                        value = value >> 1
                context_enlargeIn -= 1
                if context_enlargeIn == 0:
                    context_enlargeIn = math.pow(2, context_numBits)
                    context_numBits += 1
                del context_dictionaryToCreate[context_w]
            else:
                value = context_dictionary[context_w]
                for i in range(context_numBits):
                    context_data_val = (context_data_val << 1) | (value & 1)
                    if context_data_position == bitsPerChar - 1:
                        context_data_position = 0
                        context_data.append(getCharFromInt(context_data_val))
                        context_data_val = 0
                    else:
                        context_data_position += 1
                    value = value >> 1

            context_enlargeIn -= 1
            if context_enlargeIn == 0:
                context_enlargeIn = math.pow(2, context_numBits)
                context_numBits += 1

            # Add wc to the dictionary.
            context_dictionary[context_wc] = context_dictSize
            context_dictSize += 1
            context_w = str(context_c)

    # Output the code for w.
    if context_w != "":
        if context_w in context_dictionaryToCreate:
            if ord(context_w[0]) < 256:
                for i in range(context_numBits):
                    context_data_val = (context_data_val << 1)
                    if context_data_position == bitsPerChar-1:
                        context_data_position = 0
                        context_data.append(getCharFromInt(context_data_val))
                        context_data_val = 0
                    else:
                        context_data_position += 1
                value = ord(context_w[0])
                for i in range(8):
                    context_data_val = (context_data_val << 1) | (value & 1)
                    if context_data_position == bitsPerChar - 1:
                        context_data_position = 0
                        context_data.append(getCharFromInt(context_data_val))
                        context_data_val = 0
                    else:
                        context_data_position += 1
                    value = value >> 1
            else:
                value = 1
                for i in range(context_numBits):
                    context_data_val = (context_data_val << 1) | value
                    if context_data_position == bitsPerChar - 1:
                        context_data_position = 0
                        context_data.append(getCharFromInt(context_data_val))
                        context_data_val = 0
                    else:
                        context_data_position += 1
                    value = 0
                value = ord(context_w[0])
                for i in range(16):
                    context_data_val = (context_data_val << 1) | (value & 1)
                    if context_data_position == bitsPerChar - 1:
                        context_data_position = 0
                        context_data.append(getCharFromInt(context_data_val))
                        context_data_val = 0
                    else:
                        context_data_position += 1
                    value = value >> 1
            context_enlargeIn -= 1
            if context_enlargeIn == 0:
                context_enlargeIn = math.pow(2, context_numBits)
                context_numBits += 1
            del context_dictionaryToCreate[context_w]
        else:
            value = context_dictionary[context_w]
            for i in range(context_numBits):
                context_data_val = (context_data_val << 1) | (value & 1)
                if context_data_position == bitsPerChar - 1:
                    context_data_position = 0
                    context_data.append(getCharFromInt(context_data_val))
                    context_data_val = 0
                else:
                    context_data_position += 1
                value = value >> 1

    context_enlargeIn -= 1
    if context_enlargeIn == 0:
        context_enlargeIn = math.pow(2, context_numBits)
        context_numBits += 1

    # Mark the end of the stream
    value = 2
    for i in range(context_numBits):
        context_data_val = (context_data_val << 1) | (value & 1)
        if context_data_position == bitsPerChar - 1:
            context_data_position = 0
            context_data.append(getCharFromInt(context_data_val))
            context_data_val = 0
        else:
            context_data_position += 1
        value = value >> 1

    # Flush the last char
    while True:
        context_data_val = (context_data_val << 1)
        if context_data_position == bitsPerChar - 1:
            context_data.append(getCharFromInt(context_data_val))
            break
        else:
           context_data_position += 1

    return "".join(context_data)

def _decompress(length, resetValue, getNextValue):
    dictionary = {}
    enlargeIn = 4
    dictSize = 4
    numBits = 3
    entry = ""
    result = []

    data = Object(
        val=getNextValue(0),
        position=resetValue,
        index=1
    )

    for i in range(3):
        dictionary[i] = i

    bits = 0
    maxpower = math.pow(2, 2)
    power = 1

    while power != maxpower:
        resb = data.val & data.position
        data.position >>= 1
        if data.position == 0:
            data.position = resetValue
            data.val = getNextValue(data.index)
            data.index += 1

        bits |= power if resb > 0 else 0
        power <<= 1;

    next = bits
    if next == 0:
        bits = 0
        maxpower = math.pow(2, 8)
        power = 1
        while power != maxpower:
            resb = data.val & data.position
            data.position >>= 1
            if data.position == 0:
                data.position = resetValue
                data.val = getNextValue(data.index)
                data.index += 1
            bits |= power if resb > 0 else 0
            power <<= 1
        c = chr(bits)
    elif next == 1:
        bits = 0
        maxpower = math.pow(2, 16)
        power = 1
        while power != maxpower:
            resb = data.val & data.position
            data.position >>= 1
            if data.position == 0:
                data.position = resetValue;
                data.val = getNextValue(data.index)
                data.index += 1
            bits |= power if resb > 0 else 0
            power <<= 1
        c = chr(bits)
    elif next == 2:
        return ""

    dictionary[3] = c
    w = c
    result.append(c)
    counter = 0
    while True:
        counter += 1
        if data.index > length:
            return ""

        bits = 0
        maxpower = math.pow(2, numBits)
        power = 1
        while power != maxpower:
            resb = data.val & data.position
            data.position >>= 1
            if data.position == 0:
                data.position = resetValue;
                data.val = getNextValue(data.index)
                data.index += 1
            bits |= power if resb > 0 else 0
            power <<= 1

        c = bits
        if c == 0:
            bits = 0
            maxpower = math.pow(2, 8)
            power = 1
            while power != maxpower:
                resb = data.val & data.position
                data.position >>= 1
                if data.position == 0:
                    data.position = resetValue
                    data.val = getNextValue(data.index)
                    data.index += 1
                bits |= power if resb > 0 else 0
                power <<= 1

            dictionary[dictSize] = chr(bits)
            dictSize += 1
            c = dictSize - 1
            enlargeIn -= 1
        elif c == 1:
            bits = 0
            maxpower = math.pow(2, 16)
            power = 1
            while power != maxpower:
                resb = data.val & data.position
                data.position >>= 1
                if data.position == 0:
                    data.position = resetValue;
                    data.val = getNextValue(data.index)
                    data.index += 1
                bits |= power if resb > 0 else 0
                power <<= 1
            dictionary[dictSize] = chr(bits)
            dictSize += 1
            c = dictSize - 1
            enlargeIn -= 1
        elif c == 2:
            return "".join(result)

        if enlargeIn == 0:
            enlargeIn = math.pow(2, numBits)
            numBits += 1

        if c in dictionary:
            entry = dictionary[c]
        else:
            if c == dictSize:
                entry = w + w[0]
            else:
                return None
        result.append(entry)

        # Add w+entry[0] to the dictionary.
        dictionary[dictSize] = w + entry[0]
        dictSize += 1
        enlargeIn -= 1

        w = entry
        if enlargeIn == 0:
            enlargeIn = math.pow(2, numBits)
            numBits += 1

class LZString(object):
    @staticmethod
    def compress(uncompressed):
        return _compress(uncompressed, 16, chr)

    @staticmethod
    def compressToUint8Array(uncompressed):
        return bytes([ord(x) for x in _compress(uncompressed, 8, chr)])

    @staticmethod
    def compressToUTF16(uncompressed):
        if uncompressed is None:
            return ""
        return _compress(uncompressed, 15, lambda a: chr(a+32)) + " "

    @staticmethod
    def compressToBase64(uncompressed):
        if uncompressed is None:
            return ""
        res = _compress(uncompressed, 6, lambda a: keyStrBase64[a])
        # To produce valid Base64
        end = len(res) % 4
        if end > 0:
            res += "="*(4 - end)
        return res

    @staticmethod
    def compressToEncodedURIComponent(uncompressed):
        if uncompressed is None:
            return ""
        return _compress(uncompressed, 6, lambda a: keyStrUriSafe[a])

    @staticmethod
    def decompress(compressed):
        if compressed is None:
            return ""
        if compressed == "":
            return None
        return _decompress(len(compressed), 32768, lambda index: ord(compressed[index]))

    @staticmethod
    def decompressFromUint8Array(compressed):
        if compressed is None:
            return ""
        if compressed == "":
            return None
        return _decompress(len(compressed), 128, lambda index: compressed[index])

    @staticmethod
    def decompressFromUTF16(compressed):
        if compressed is None:
            return ""
        if compressed == "":
            return None
        return _decompress(len(compressed), 16384, lambda index: compressed[index] - 32)

    @staticmethod
    def decompressFromBase64(compressed):
        if compressed is None:
            return ""
        if compressed == "":
            return None
        return _decompress(len(compressed), 32, lambda index: getBaseValue(keyStrBase64, compressed[index]))

    @staticmethod
    def decompressFromEncodedURIComponent(compressed):
        if compressed is None:
            return ""
        if compressed == "":
            return None
        compressed = compressed.replace(" ", "+")
        return _decompress(len(compressed), 32, lambda index: getBaseValue(keyStrUriSafe, compressed[index]))


code:
@echo off
:: run a Node based web server

:: Set working directory
pushd %~dp0
SET BASEDIR=%~dp0

:: run dependencyCheck.bat
CALL .\devTools\scripts\dependencyCheck.bat
SET CODE=%ERRORLEVEL%

IF %CODE% EQU 69 (
	:: if exit code is 69, then we don't have all the dependencies we need
	ECHO.
	ECHO Dependencies not met.
    ECHO.
	EXIT /b 0
) ELSE IF %CODE% EQU 0 (
	:: if exit code is 0, run web server
	CALL npx http-server --port 6969 -c-1
	EXIT /b 0
) ELSE (
	:: if exit code is not 0, print error message
	ECHO.
	ECHO dependencyCheck.bat exited with code: %CODE%
	ECHO Dependency check failed.
    ECHO.
    EXIT /b 0
)

repo_name: Nithhogg/fc

license: mit


code:
#!/bin/bash

# run a Node based web server

# run dependencyCheck.sh
./devTools/scripts/dependencyCheck.sh
exitCode=$?
# exit code is now stored in $exitCode

# if exit code is 69, then we don't have all the dependencies we need
if [[ $exitCode -eq 69 ]]; then
    echo "Dependencies not met."
    echo ""
   exit 0
# if exit code is not 0, print error message
elif [[ $exitCode -ne 0 ]]; then
	echo "dependencyCheck.sh exited with code: $exitCode"
    echo "Dependency check failed."
    echo ""
    exit 0
# if exit code is 0, run web server
else
    npx http-server --port 6969 -c-1
fi


code:
@ECHO off
:: Free Cities Settings Menu - Windows

:: run dependencyCheck.bat
CALL .\devTools\scripts\dependencyCheck.bat
SET CODE=%ERRORLEVEL%

IF %CODE% EQU 69 (
	:: if exit code is 69, then we don't have all the dependencies we need
	ECHO.
	ECHO Dependencies not met.
	ECHO If the only thing you need is to simply compile FC, then you can use 'simple-compiler.bat' instead.
    ECHO.
	EXIT /b 0
) ELSE IF %CODE% EQU 0 (
	:: if exit code is 0, run setup.js
	CALL node devTools\scripts\setup.js
	EXIT /b 0
) ELSE (
	:: if exit code is not 0, print error message
	ECHO.
	ECHO dependencyCheck.bat exited with code: %CODE%
	ECHO Dependency check failed.
	ECHO If the only thing you need is to simply compile FC, then you can use 'simple-compiler.bat' instead.
    ECHO.
    EXIT /b 0
)


code:
#!/bin/bash

# Free Cities Settings Menu - Unix

# run dependencyCheck.sh
./devTools/scripts/dependencyCheck.sh
exitCode=$?
# exit code is now stored in $exitCode

# if exit code is 69, then we don't have all the dependencies we need
if [[ $exitCode -eq 69 ]]; then
    echo "Dependencies not met."
    echo "If the only thing you need is to simply compile FC, then you can use 'simple-compiler.bat' instead."
    echo ""
   exit 0
# if exit code is not 0, print error message
elif [[ $exitCode -ne 0 ]]; then
	echo "dependencyCheck.sh exited with code: $exitCode"
    echo "Dependency check failed."
    echo "If the only thing you need is to simply compile FC, then you can use 'simple-compiler.bat' instead."
    echo ""
    exit 0
# if exit code is 0, run setup.js
else
    node devTools/scripts/setup.js
fi


code:
@echo off
:: Free Cities Basic Compiler - Windows

:: Set working directory
pushd %~dp0
SET BASEDIR=%~dp0
SETLOCAL EnableDelayedExpansion

:: process arguments from command line
:processargs
SET ARG=%1
IF DEFINED ARG (
	:: legacy arguments
    IF "%ARG%"=="EXTRAMEMORY" SET SORT_MEM=65535
	IF "%ARG%"=="EMEMORY" SET SORT_MEM=65535
	IF "%ARG%"=="EXTRAMEM" SET SORT_MEM=65535
	IF "%ARG%"=="--extramem" SET SORT_MEM=65535
	:: create theme
	if "%ARG%"=="--themes" SET "THEMES=True"
	if "%ARG%"=="--help" GOTO :help
	:: exit without user input
	if "%ARG%"=="--no-wait" SET "NOWAIT=True"
    SHIFT
    GOTO processargs
)

:legacybuild

:: if the bin directory doesn't exist, then create it
if NOT EXIST .\bin\ (
	MKDIR .\bin\
)

if DEFINED THEMES (
	CALL :compile_themes
)

ECHO Compiling FC...
ECHO Press Ctrl+C at any time to cancel the build.

:: See if we can find a git installation
set GITFOUND=no
for %%k in (HKCU HKLM) do (
	for %%w in (\ \Wow6432Node\) do (
		for /f "skip=2 delims=: tokens=1*" %%a in ('reg query "%%k\SOFTWARE%%wMicrosoft\Windows\CurrentVersion\Uninstall\Git_is1" /v InstallLocation 2^> nul') do (
			for /f "tokens=3" %%z in ("%%a") do (
				set GIT=%%z:%%b
				set GITFOUND=yes
				goto FOUND
			)
		)
	)
)
:FOUND
if %GITFOUND% == yes (
	set "PATH=%GIT%bin;%PATH%"
	echo|set /p out="App.Version.commitHash = " > "%~dp0src\002-config\fc-version.js.commitHash.js"
	git rev-parse --sq --short HEAD >> "%~dp0src\002-config\fc-version.js.commitHash.js" 2>NUL
	if errorlevel 1 echo|set /p out="null" >> "%~dp0src\002-config\fc-version.js.commitHash.js"
	echo|set /p out=";" >> "%~dp0src\002-config\fc-version.js.commitHash.js"
)

if not exist "bin\resources" mkdir bin\resources
CALL devTools/concatFiles.bat js\ "*.js" bin\fc.js
CALL devTools/concatFiles.bat css\ "*.css" bin\fc.css
SET TWEEGO_PATH=%~dp0devTools\tweeGo\storyFormats
:: Run the appropriate compiler for the user's CPU architecture.
if %PROCESSOR_ARCHITECTURE% == AMD64 (
	CALL "%~dp0devTools\tweeGo\tweego_win64.exe" -o "%~dp0bin/FC_pregmod.html" --module=bin/fc.js --module=bin/fc.css --head resources/raster/favicon/arcologyVector.html "%~dp0src"
) else (
	CALL "%~dp0devTools\tweeGo\tweego_win86.exe" -o "%~dp0bin/FC_pregmod.html" --module=bin/fc.js --module=bin/fc.css --head resources/raster/favicon/arcologyVector.html "%~dp0src"
)
DEL bin\fc.js
DEL bin\fc.css
IF EXIST "%~dp0src\002-config\fc-version.js.commitHash.js" DEL "%~dp0src\002-config\fc-version.js.commitHash.js"

ECHO Done

:: keep window open instead of closing it
if NOT DEFINED NOWAIT (
	<nul set /p "=Press any key to exit"
	pause >nul
)

ENDLOCAL
popd
GOTO :eof

:compile_themes
ECHO Compiling Themes...

set back=%cd%
for /d %%i in (%~dp0\themes\*) do (
	CALL :compileDirectory %%i
)
cd %back%

EXIT /B %ERRORLEVEL%

:compileDirectory
REM ~1 is an absolute path, get name of directory here
REM https://stackoverflow.com/a/5480568
set var1=%~1%
set var2=%var1%
set i=0

:loopprocess
for /F "tokens=1* delims=\" %%A in ( "%var1%" ) do (
	set /A i+=1
	set var1=%%B
	goto loopprocess
)

for /F "tokens=%i% delims=\" %%G in ( "%var2%" ) do set last=%%G

REM compile
CALL devTools/concatFiles.bat "%%~1" "*.css" bin\"%last%".css
:: end loopprocess and compileDirectory
GOTO :eof

:help
ECHO compile.bat [flags]
ECHO.
ECHO --themes: create themes
ECHO --extramem: passed to sort (as SORT_MEM) in devTools/concatFiles.bat
ECHO --no-wait: exit without user input
:: exit
ENDLOCAL
popd
exit /b 0


code:
#!/bin/bash

output=/dev/stdout
name=FC_pregmod

# displays help text
function displayHelp() {
	cat <<HelpText
Usage: compile.sh [OPTION]...

Options:
  -d, --dry      Do not compile
  -g, --git      Add hash of HEAD to filename
  -h, --help     Show this help text
  -s, --sanity   Run sanityCheck
  -q, --quiet    Suppress terminal output
  -t, --themes   Generate theme files
  -m, --minify   Minify output files
  --ci           CI mode
  -f, --f        Final file name
HelpText
}

#display an error message
function echoError() {
	echo -e "\033[0;31m$*\033[0m"
}

#display message
function echoMessage() {
	echo "$1" >"${output}"
}

#compile the HTML file
function compile() {
	mkdir -p bin/resources
	export TWEEGO_PATH=devTools/tweeGo/storyFormats
	TWEEGO_EXE="tweego"

	if hash $TWEEGO_EXE 2>/dev/null; then
		echoMessage "system tweego binary"
	else
		case "$(uname -m)" in
			x86_64 | amd64)
				echoMessage "x64 arch"
				if [ "$(uname -s)" = "Darwin" ]; then
					TWEEGO_EXE="./devTools/tweeGo/tweego_osx64"
				elif [ "$OSTYPE" = "msys" ]; then
					TWEEGO_EXE="./devTools/tweeGo/tweego_win64"
				else
					TWEEGO_EXE="./devTools/tweeGo/tweego_nix64"
				fi
				;;
			x86 | i[3-6]86)
				echoMessage "x86 arch"
				if [ "$(uname -s)" = "Darwin" ]; then
					TWEEGO_EXE="./devTools/tweeGo/tweego_osx86"
				elif [ "$OSTYPE" = "msys" ]; then
					TWEEGO_EXE="./devTools/tweeGo/tweego_win86"
				else
					TWEEGO_EXE="./devTools/tweeGo/tweego_nix86"
				fi
				;;
			*)
				echoError "No system tweego binary found, and no precompiled binary for your platform available."
				echoError "Please compile tweego and put the executable in PATH."
				exit 2
				;;
		esac
	fi

	if [ "$(uname -m)" = "x86_64" ] || [ "$(uname -m)" = "amd64" ]; then
    if [ "$(uname -s)" = "Darwin" ]; then
      MINIFY_EXE="./devTools/minify/minify_darwin_amd64"
    elif [ "$OSTYPE" = "msys" ]; then
      MINIFY_EXE="./devTools/minify/minify_win_amd64.exe"
    else
      MINIFY_EXE="./devTools/minify/minify_linux_amd64"
    fi
	fi

	file="bin/${name}.html"

	# Find and insert current commit
	if [[ "$ci" ]]; then
		COMMIT=$CI_COMMIT_SHORT_SHA
	elif [[ -d .git ]]; then
		COMMIT=$(git rev-parse --short HEAD)
		if [[ "$usehash" ]]; then
			file="bin/${name}${COMMIT}.html"
		fi
	fi
	if [[ "$minify" ]]; then
		final_file=$file
		file="bin/tmp.html"
	fi

	if [[ "$COMMIT" ]]; then
		printf "App.Version.commitHash = '%s';\n" "${COMMIT}" > src/002-config/fc-version.js.commitHash.js
	fi

	devTools/concatFiles.sh js/ '*.js' bin/fc.js
	devTools/concatFiles.sh css/ '*.css' bin/fc.css
	$TWEEGO_EXE -o "$file" --module=bin/fc.js --module=bin/fc.css --head resources/raster/favicon/arcologyVector.html src/ || build_failed="true"
	rm -f bin/fc.js
	rm -f bin/fc.css
	if [ "$build_failed" = "true" ]; then
		echoError "Build failed."
		exit 1
	fi

	if [[ "$minify" ]]; then
		if [[ "$MINIFY_EXE" ]]; then
			# SC license is inside an HTML comment, so keep them.
			# eval depends on local variables, so don't modify them (Config, Engine, ...)
			$MINIFY_EXE --html-keep-comments --js-keep-var-names "$file" > "$final_file"
			rm -f "$file"
		else
			echoError "Minification only available on amd64 systems."
			mv "$file" "$final_file"
		fi
		file=$final_file
	fi

	if [[ "$ci" || -d .git ]]; then
		rm src/002-config/fc-version.js.commitHash.js
	fi
	echoMessage "Saved to $file."
}

if [[ "$1" == "" ]]; then
	#tip if no option
	echoMessage "For more options see compile.sh -h."
else
	#parse options
	while [[ "$1" ]]; do
		case $1 in
			-d | --dry)
				dry="true"
				;;
			-g | --git)
				usehash="true"
				;;
			-h | --help)
				displayHelp
				exit 0
				;;
			-s | --sanity)
				sanity="true"
				;;
			-q | --quiet)
				output=/dev/null
				;;
			-t | --themes)
				themes="true"
				;;
			-m | --minify)
				minify="true"
				;;
			--ci)
				ci="true"
				;;
			-f | --file)
				name=$2
				shift
				;;
			*)
				echoError "Unknown argument $1."
				displayHelp
				exit 1
				;;
		esac
		shift
	done
fi

# Run sanity check.
[ -n "$sanity" ] && ./sanityCheck.sh

if ! [[ -d .git ]]; then
	echoMessage "No git repository. Git specific actions disabled."
fi

#compile
if [[ "$dry" ]]; then
	echoMessage "Dry run finished."
else
	compile
	echoMessage "Compilation finished at $(date +%T)."
fi

# compile themes
if [[ "$themes" ]]; then
	(
		cd themes/ || exit
		for D in *; do
			if [ -d "${D}" ]; then
				../devTools/concatFiles.sh "${D}"/ '*.css' ../bin/"${D}".css
			fi
		done
	)
	echoMessage "Themes compiled at $(date +%T)"
fi


code:
/**
 * The basic class for all classes that can be saved and loaded, i.e. serialized and deserialized.
 *
 * All subclasses HAVE to implement get className(), _cleanUpConfigScheme() and clone(),
 * in the SAME WAY they are implemented here.
 */
App.Entity.Serializable = class {
	/**
	 * Returns the name of the class including namespaces
	 *
	 * @returns {string}
	 */
	get className() { return "App.Entity.Serializable"; }

	/**
	 * Updates saved data in case of changes to the class.
	 *
	 * NOTE: new attributes do NOT need to be added here, as they are automatically added with default values.
	 *
	 * @param {object} config
	 * @protected
	 */
	static _cleanupConfigScheme(config) {
		// in subclasses _cleanupConfigScheme() must call
		// super._cleanupConfigScheme(config)
		// BC code
	}

	/**
	 * @returns {App.Entity.Serializable}
	 */
	clone() {
		return (new App.Entity.Serializable())._init(this);
	}

	/**
	 * @param {object} config
	 * @param {boolean} clean
	 * @returns {this}
	 * @protected
	 */
	_init(config, clean = false) {
		if (clean) {
			this.constructor._cleanupConfigScheme(config);
		}

		// Clone the given object's own properties into our own properties.
		deepAssign(this, config);

		// Return `this` to make usage easier.
		return this;
	}

	/**
	 * @returns {[]}
	 */
	toJSON() {
		// Return a code string that will create a new instance containing our
		// own data.
		//
		// NOTE: Supplying `this` directly as the `reviveData` parameter to the
		// `Serial.createReviver()` call will trigger out of control recursion in
		// the serializer, so we must pass it a clone of our own data instead.
		const ownData = {};
		deepAssign(ownData, this);
		return Serial.createReviver(`(new ${this.className}())._init($ReviveData$, true)`, ownData);
	}
};


code:
App.Version = {
	base: "0.10.7.1", // The vanilla version the mod is based off of, this should never be changed.
	pmod: "4.0.0-alpha.35",
	commitHash: null,
	/**
	 * @type {number} used for patching via BC
	 * See {@link App.Patch.applyAll}
	 *
	 * When you get to release 2000, use 2001 instead.
	 * The release numbers got messed up, this is corrected in `src/js/eventHandlers.js` and `/src/data/patches/patch.js`.
	 * The two line above and this line should be safe to remove after release 2001.
	 */
	release: 1284,
};


code:
/**
 * Expand mousetrap with multiple binds per key, passage dependent bindings and a menu for custom rebinding.
 */
App.UI.Hotkeys = (function() {
	/**
	 * @typedef action
	 * @property {Function} callback
	 * @property {Array<string>} combinations 0 <= length <= 2
	 * @property {Array<string>} [passages] not existing means everywhere
	 * @property {string|function(): string} [uiName] allow different name in hotkey settings
	 */

	/**
	 * The key is used in the hotkey settings to describe the action
	 * @type {{[key: string]: action}}
	 */
	const actions = {};

	/**
	 * Contains the default combinations for every action
	 * @type {{[key: string]: Array<string>}}
	 */
	const defaultCombinations = {};

	/**
	 * References a key combination to a set of actions
	 * @type {{[key: string]: Array<string>}}
	 */
	const bindings = {};

	/**
	 * To ensure we only record one at a time
	 * @type {boolean}
	 */
	let recording = false;

	/**
	 * @param {string} name used as key
	 * @param {action} action
	 */
	function addDefault(name, action) {
		actions[name] = action;
		for (const binding of action.combinations) {
			addBinding(name, binding);
		}
		defaultCombinations[name] = [...action.combinations];
	}

	/**
	 * @param {string} actionKey
	 * @param {string} combination
	 */
	function addBinding(actionKey, combination) {
		if (bindings[combination]) {
			bindings[combination].push(actionKey);
		} else {
			bindings[combination] = [actionKey];
			Mousetrap.bind(combination, e => {
				e.preventDefault();
				for (const binding of bindings[combination]) {
					const action = actions[binding];
					// only activate callback if we are on the right passage
					if (!action.passages || action.passages.includes(State.passage)) {
						action.callback();
					}
				}
			});
		}
	}

	/**
	 * @param {string} actionKey
	 * @param {string} combination
	 */
	function removeBinding(actionKey, combination) {
		if (bindings[combination]) {
			const index = bindings[combination].indexOf(actionKey);
			if (index > -1) {
				bindings[combination].splice(index, 1);
				if (bindings[combination].length === 0) {
					delete bindings[combination];
					Mousetrap.unbind(combination);
				}
			}
		}
	}

	/**
	 * @param {string} name
	 * @returns {string}
	 */
	function hotkeysForAction(name) {
		if (!actions[name]) {
			return "";
		}
		const c = actions[name].combinations;
		if (c.length === 0) {
			return "";
		}
		if (c.length === 1) {
			return `[${formatHotkey(c[0])}]`;
		}
		return `[${formatHotkey(c[0])},${formatHotkey(c[1])}]`;
	}

	/**
	 * @param {string} combination
	 * @returns {string}
	 */
	function formatHotkey(combination) {
		const parts = combination.split("+");

		for (let i = 0; i < parts.length; i++) {
			parts[i] = capFirstChar(parts[i]);
		}

		return parts.join("+");
	}

	/**
	 * @returns {HTMLDivElement}
	 */
	function settingsMenu() {
		const div = document.createElement("div");
		div.className = "hotkey-settings";

		for (const actionsKey in actions) {
			settingsRow(div, actionsKey);
		}

		return div;
	}

	/**
	 * @param {HTMLDivElement} container
	 * @param {string} actionKey
	 */
	function settingsRow(container, actionKey) {
		const action = actions[actionKey];
		// get correct name
		let name = actionKey;
		if (action.uiName) {
			if (typeof action.uiName === "string") {
				name = action.uiName;
			} else {
				name = action.uiName();
			}
		}
		App.UI.DOM.appendNewElement("div", container, name, "description");

		settingsCell(container, actionKey, 0);
		settingsCell(container, actionKey, 1);

		const button = App.UI.DOM.appendNewElement("button", container, "Reset");
		if (isDefault(actionKey)) {
			button.disabled = true;
		} else {
			button.onclick = () => {
				action.combinations = [...defaultCombinations[actionKey]];
				saveToStorage();
				App.UI.reload();
			};
		}
	}

	/**
	 * Checks if the combinations assigned to an action are the default ones.
	 * @param {string} actionKey
	 * @returns {boolean}
	 */
	function isDefault(actionKey) {
		if (defaultCombinations[actionKey].length !== actions[actionKey].combinations.length) {
			return false;
		}
		if (defaultCombinations[actionKey].length === 0) {
			return true;
		}
		if (defaultCombinations[actionKey][0] !== actions[actionKey].combinations[0]) {
			return false;
		}
		if (defaultCombinations[actionKey].length === 1) {
			return true;
		}
		return defaultCombinations[actionKey][1] === actions[actionKey].combinations[1];
	}

	/**
	 * @param {HTMLDivElement} container
	 * @param {string} actionKey
	 * @param {number} index
	 */
	function settingsCell(container, actionKey, index) {
		const action = actions[actionKey];
		const button = App.UI.DOM.appendNewElement("button", container,
			action.combinations[index] ? formatHotkey(action.combinations[index]) : "", "combination");
		button.onclick = () => {
			if (recording) { return; }
			recording = true;

			$(button).empty();
			Mousetrap.record(function(sequence) {
				// sequence is an array like ['ctrl+k', 'c']
				const combination = sequence.join(" ");
				if (action.combinations[index]) {
					removeBinding(actionKey, action.combinations[index]);
				}
				action.combinations[index] = combination;
				addBinding(actionKey, combination);
				saveToStorage();
				App.UI.reload();
				recording = false;
			});
		};
	}

	/**
	 * Saves custom hotkeys to browser storage
	 */
	function saveToStorage() {
		const save = {};

		for (const actionsKey in actions) {
			if (!isDefault(actionsKey)) {
				save[actionsKey] = actions[actionsKey].combinations;
			}
		}

		SugarCube.storage.set("hotkeys", save);
	}

	/**
	 * Loads custom hotkeys from browser storage
	 */
	function loadFromStorage() {
		const save = SugarCube.storage.get("hotkeys");

		for (const saveKey in save) {
			// discard obsolete hotkeys
			if (actions[saveKey]) {
				actions[saveKey].combinations = save[saveKey];
				addBinding(saveKey, save[saveKey]);
			}
		}
	}

	/**
	 * Initialize custom hotkeys
	 */
	function init() {
		loadFromStorage();
		// :storyready is to late to influence the page, but it's the earliest where SugarCube.storage is available so
		// we refresh the passage if we happen to be on the settings passage.
		if (State.passage === "Hotkey Settings") {
			App.UI.reload();
		}
	}

	function settingsPage() {
		const f = document.createDocumentFragment();

		App.UI.DOM.appendNewElement("h1", f, "Hotkey Settings");

		const p = document.createElement("p");
		const ul = document.createElement("ul");

		let li = document.createElement("li");
		li.append("On keyboard layouts other than the ");
		const a = document.createElement("a");
		a.classList.add("link-external");
		a.href = "https://en.wikipedia.org/wiki/File:KB_United_States.svg";
		a.target = "_blank";
		a.append("US-QWERTY layout");
		li.append(a,
			" there may be keys or combinations of keys where the recorded key is different from the key used to listen to key events. You will have to find these keys yourself through trial and error.");
		ul.append(li);

		App.UI.DOM.appendNewElement("li", ul, "Custom hotkeys are browser specific and are not part of your save.");

		li = document.createElement("li");
		li.append("While we try not to overwrite browser or OS level key combinations it is possible to do so with custom hotkeys. This also means that during recording of custom hotkeys no browser or OS level key combinations are available. There are however keys that cannot be overwritten, such as the ",
			App.UI.DOM.makeElement("code", "Win key"), " on Windows.");
		ul.append(li);

		p.append(ul);
		f.append(p);

		f.append(settingsMenu());
		return f;
	}

	return {
		add: addDefault,
		hotkeys: hotkeysForAction,
		init: init,
		settings: settingsPage,
	};
})();

// add hotkeys
App.UI.Hotkeys.add("endWeek", {
	callback: function() {
		$("#story-caption #endWeekButton").trigger("click");
	}, combinations: ["enter"], uiName: "Next Week"
});
App.UI.Hotkeys.add("nextLink", {
	callback: function() {
		$("#story-caption #nextButton").trigger("click");
	}, combinations: ["space"], uiName: "Continue/Back"
});
App.UI.Hotkeys.add("prev-slave", {
	callback: function() {
		$("#prev-slave a").trigger("click");
	}, combinations: ["left", "q"], uiName: "Previous Slave"
});
App.UI.Hotkeys.add("next-slave", {
	callback: function() {
		$("#next-slave a").trigger("click");
	}, combinations: ["right", "e"], uiName: "Next Slave"
});
App.UI.Hotkeys.add("prev-child", {
	callback: function() {
		$("#prev-child a.macro-link").trigger("click");
	}, combinations: ["left", "q"], uiName: "Previous Child"
});
App.UI.Hotkeys.add("next-child", {
	callback: function() {
		$("#next-child a.macro-link").trigger("click");
	}, combinations: ["right", "e"], uiName: "Next Child"
});
App.UI.Hotkeys.add("Previous Tab", {
	callback: function() {
		App.UI.Tabs.left();
	}, combinations: []
});
App.UI.Hotkeys.add("Next Tab", {
	callback: function() {
		App.UI.Tabs.right();
	}, combinations: []
});
App.UI.Hotkeys.add("Close Dialog", {
	callback: function() {
		Dialog.close();
	}, combinations: ["esc"]
});
App.UI.Hotkeys.add("walkpast", {
	callback: function() {
		$("#walkpast a").trigger("click");
	}, combinations: [], uiName: "Walkpast"
});
App.UI.Hotkeys.add("Head Girl Select", {
	callback: function() {
		$("#manageHG a").trigger("click");
	}, combinations: ["h"]
});
App.UI.Hotkeys.add("Previous AI Image", {
	callback: function() {
		$(".lrgRender .ai-art-container .leftArrow").trigger("click");
	}, combinations: ["ctrl+left"]
});
App.UI.Hotkeys.add("Next AI Image", {
	callback: function() {
		$(".lrgRender .ai-art-container .rightArrow").trigger("click");
	}, combinations: ["ctrl+right"]
});


code:
:: StoryData
{
	"ifid":"18DFC483-910E-4D3B-AC31-71AE10C5015A",
	"format": "SugarCube",
	"format-version": "2.36.0"
}

:: Start [nobr]

:: StoryTitle
Free Cities


code:
/* eslint-disable no-undef */
"use strict";
/* Main SugarCube configuration file. */

/* Change the starting passage from the default 'start' to 'Alpha disclaimer'. */
Config.passages.start = "Alpha disclaimer";

/* Set description used by Save, for all passages, to give some decent information about game state. */
Config.saves.descriptions = function() {
	let sv = State.variables;
	if (sv.arcologies === undefined || sv.arcologies.length === 0) {
		// no arcology yet...
		return "New Game Setup, Week 0";
	} else {
		return `${App.Utils.isEndWeek() ? "EW! " : ""}${sv.arcologies[0].name}, Week ${sv.week}, ${sv.slaves.size} Slaves, ${cashFormat(sv.cash)}`;
	}
};

/* Disable forward/back buttons in panel. */
Config.history.controls = false;

/* Set Autosaves. */
Config.saves.maxAutoSaves = 1;

/* Save only one game state. */
Config.history.maxStates = 1;

/* Set to 'true' to enable SugarCube's debug mode.
Note: This is an 'engine level' debug mode, completely separate from the game's debug mode. */
Config.debug = false;

/* Set maximum loop iterations. Among other things, this controls the maximum number of slaves the player can own. */
Config.macros.maxLoopIterations = 5000;


code:
// Use release as save version
// We need to do this in a file after fc-version.js because the commit hash gets appended to it during compiling
Config.saves.version = `${App.Version.release}${App.Version.commitHash ? (`#${App.Version.commitHash}`) : ''}`;


code:
(function() {
	const readOnlySymbol = Symbol("readonly proxy");
	const dummy = () => true;

	/**
	 * Creates a readonly proxy for all the target properties except banned methods
	 * @param {any} target
	 * @param {string[]} bannedMethodNames The list of method names that are banned and can't be accesses via the proxy
	 * @param {string} errorMessage message for throwing exceptions
	 */
	const createProxyWithBannedMethods = (target, bannedMethodNames, errorMessage) => {
		return new Proxy(target, {
			get: function(o, prop) {
				if (prop === readOnlySymbol) { return true; }
				const val = o[prop];
				if (typeof val === 'function') {
					if (bannedMethodNames.includes(prop)) {
						return function() {
							throw Error(errorMessage);
						};
					}
					return val.bind(target);
				}
				return createReadonlyProxy(val);
			},
			set: dummy,
			deleteProperty: dummy
		});
	};

	globalThis.createReadonlyProxy = function(target) {
		if (target == null) { return target; } // intentionally ==
		if (target[readOnlySymbol]) { return target; }
		if (_.isArray(target)) {
			return createProxyWithBannedMethods(target, ['push', 'unshift', 'pop'], "Cannot modify a readonly array");
		} else if (_.isMap(target)) {
			return createProxyWithBannedMethods(target, ['clear', 'delete', 'set'], "Cannot modify a readonly Map");
		} else if (_.isSet(target)) {
			return createProxyWithBannedMethods(target, ['add', 'clear', 'delete'], "Cannot modify a readonly Set");
		} else if (_.isObject(target)) {
			return new Proxy(target, {
				get: function(o, prop) {
					if (prop === readOnlySymbol) { return true; }
					return createReadonlyProxy(o[prop]);
				},
				set: dummy,
				deleteProperty: dummy
			});
		}
		return target;
	};
	const cheaterSymbol = Symbol("cheating proxy");
	globalThis.createCheatProxy = function(target) {
		if (target == null) { return target; } // intentionally ==
		if (target[cheaterSymbol]) { return target; }
		if (_.isArray(target)) {
			return new Proxy(target, {
				get: function(o, prop) {
					if (prop === cheaterSymbol) { return true; }
					const val = o[prop];
					if (typeof val === 'function') {
						if (['push', 'unshift', 'pop'].includes(prop)) {
							return function(el) {
								const retval = Array.prototype[prop].apply(o, arguments);
								// Make sure we set cheater after calling the function
								State.variables.cheater = 1;// Can't use `V` because it probably points to a proxy.
								return retval;
							};
						}
						return val.bind(target);
					}
					return createCheatProxy(val);
				},
				set: function(o, prop, value) {
					o[prop] = value;
					State.variables.cheater = 1;// Can't use `V` because it probably points to a proxy.
					return true;
				},
				deleteProperty: function(o, prop) {
					delete o[prop];
					State.variables.cheater = 1;// Can't use `V` because it probably points to a proxy.
					return true;
				}
			});
		}
		if (_.isObject(target)) {
			return new Proxy(target, {
				get: function(o, prop) {
					if (prop === cheaterSymbol) { return true; }
					return createCheatProxy(o[prop]);
				},
				set: function(o, prop, value) {
					o[prop] = value;
					State.variables.cheater = 1;
					return true;
				},
				deleteProperty: function(o, prop) {
					delete o[prop];
					State.variables.cheater = 1;
					return true;
				}
			});
		}
		return target;
	};
})();
Object.defineProperty(window, "V", {
	get: function() {
		return window.storyProxy ?? State.variables;
	}
});

/**
 * This should be used if the user might use V under normal, non-cheating circumstances but shouldn't be punished for accidentally setting the value. The attempt to make the change will simply be disregarded.
 * @template {Function} F
 * @param {F} callback
 * @returns {ReturnType<F>}
 */
globalThis.runWithReadonlyProxy = function(callback) {
	globalThis.storyProxy = createReadonlyProxy(State.variables);
	try {
		return callback();
	} finally {
		globalThis.storyProxy = null;
	}
};

/**
 * This should be used if setting values would constitute cheating. For example, a debug view that shows all variables in an editable form; showing isn't cheating, but making a change would be.
 * @template {Function} F
 * @param {F} callback
 * @returns {ReturnType<F>}
 */
globalThis.runWithCheatProxy = function(callback) {
	globalThis.storyProxy = createCheatProxy(State.variables);
	try {
		return callback();
	} finally {
		globalThis.storyProxy = null;
	}
};


code:
App.Arcology.Cell.BaseCell = class extends App.Entity.Serializable {
	/**
	 * @param {number} owner
	 */
	constructor(owner) {
		super();
		/**
		 * 0: private
		 * 1: player
		 * @type {number}
		 */
		this.owner = owner;
		/**
		 * List of classes this cell can be converted to.
		 * Format: "BaseCell" stands for App.Arcology.Cell.BaseCell
		 * Note: The current class needs to be included as well, otherwise converting back is not possible.
		 * @type {string[]}
		 */
		this.allowedConversions = [];
	}

	/**
	 * @returns {string}
	 */
	static get cellName() {
		return "baseCell";
	}

	/**
	 * @returns {string}
	 */
	get colorClass() {
		return "empty";
	}

	/**
	 * @returns {number}
	 */
	get width() {
		return 1;
	}

	/**
	 * @returns {string}
	 */
	get name() {
		return "Base Cell";
	}

	/**
	 * @returns {boolean}
	 */
	isBaseType() {
		return true;
	}

	renderCell(path, width) {
		const outerCell = document.createElement("div");
		outerCell.classList.add("outerCell");
		outerCell.style.width = `${width * this.width}%`;

		const innerCell = document.createElement("div");
		innerCell.classList.add("innerCell");
		innerCell.classList.add(this.owner === 1 ? this.colorClass : "private");
		innerCell.append(this.cellContent(path));

		outerCell.append(innerCell);

		return outerCell;
	}

	/**
	 * @param {Array<number>} path
	 * @returns {Node}
	 */
	cellContent(path) {
		return document.createDocumentFragment();
	}

	/**
	 * @param {App.Arcology.Building} containingBuilding
	 * @returns {DocumentFragment}
	 */
	cellPassage(containingBuilding) {
		/**
		 * Thanks JS!
		 * @type {App.Arcology.Cell.BaseCell}
		 */
		const that = this;

		const fragment = document.createDocumentFragment();

		const scene = document.createElement("p");
		scene.classList.add("scene-intro");
		scene.append(this._setting());
		if (this.canBeSold()) {
			scene.append(' ', ownership(this));
		}
		fragment.append(scene);

		if (this.owner === 1) {
			const upgrades = document.createElement("p");

			upgrades.append(this._body(containingBuilding));

			fragment.append(upgrades);

			if (this.allowedConversions.length > 0 && this.isBaseType()) {
				const p = document.createElement("p");
				if (V.rep < 5000) {
					fragment.append(App.UI.DOM.makeElement("p", "You don't have the reputation required to convert the sector base type."));
				} else {
					const cost = 50000;
					const rep = 5000;
					for (const ac of this.allowedConversions) {
						const cellClass = App.Arcology.Cell[ac];
						if (!(this instanceof cellClass)) {
							p.append(App.UI.DOM.link(`Convert sector to ${cellClass.cellName}.`, () => {
								if (Dialog.isOpen()) {
									Dialog.close();
								}

								/** @type {App.Arcology.Cell.BaseCell} */
								const newCell = new cellClass(1);
								newCell.allowedConversions = this.allowedConversions;
								containingBuilding.replaceCell(this, newCell);
								cashX(-cost, "capEx");
								repX(-rep, "capEx");

								containingBuilding.refresh();
								Dialog.create(newCell.name);
								$(Dialog.body()).empty().append(newCell.cellPassage(containingBuilding));
								Dialog.open();
							}));

							App.UI.DOM.appendNewElement("span", p, ` Costs ${cashFormat(cost)} and ${rep} reputation as many citizens will lose most of what they own.`);
						}
					}
				}
				fragment.append(p);
			}
		}

		return fragment;

		/**
		 * @param {App.Arcology.Cell.BaseCell} cell
		 * @returns {DocumentFragment}
		 */
		function ownership(cell) {
			const DEMAND_FACTOR_DIFF = 20;
			const fragment = document.createDocumentFragment();
			const A = V.arcologies[0];
			const allCells = V.building.findCells(cell => !(cell instanceof App.Arcology.Cell.Filler));
			const ownedCells = allCells.filter(cell => cell.owner === 1).length;
			const oneCellPercentage = ((1 / allCells.length) * 100).toPrecision(3);

			/**
			 * @param {boolean} buy
			 * @returns {number}
			 */
			function price(buy) {
				let demand = A.demandFactor;
				if (buy) {
					demand += DEMAND_FACTOR_DIFF;
				}
				return 1000 * Math.trunc(A.prosperity * (1 + (demand / 100)));
			}

			if (cell.owner === 1) {
				App.Events.addNode(fragment, [`Selling this sector would relinquish a ${oneCellPercentage}% interest in ${A.name}. Such an interest is worth <span class='cash'>${cashFormat(price(false))}.</span>`]);

				if (ownedCells > 1) {
					const span = document.createElement("span");
					span.classList.add("clear-formatting");
					span.append(App.Arcology.getCellLink("Sell", that.name, () => that.cellPassage(containingBuilding),
						() => {
							cashX(price(false), "capEx");
							A.demandFactor -= DEMAND_FACTOR_DIFF;
							cell.owner = 0;
							App.Arcology.updateOwnership();
						}));
					fragment.append(" ", span);
				}
			} else {
				App.Events.addNode(fragment, [`You will have to acquire an additional ${oneCellPercentage}% interest in ${A.name} to take control of this sector. Such an interest is worth ${cashFormatColor(price(true))} and will require a transaction cost of ${cashFormatColor(10000)} to acquire for a total cost of ${cashFormat(price(true) + 10000)}.`]);
				const availableCells = allCells.length * ((100 - A.minority) / 100) - ownedCells;
				if (availableCells >= 1) {
					const buySpan = document.createElement("span");
					buySpan.classList.add("clear-formatting");
					buySpan.append(App.Arcology.getCellLink("Buy", that.name, () => that.cellPassage(containingBuilding),
						() => {
							cashX(-(price(true) + 10000), "capEx");
							A.demandFactor += DEMAND_FACTOR_DIFF;
							cell.owner = 1;
							App.Arcology.updateOwnership();
						}));
					fragment.append(" ", buySpan);

					if (V.rep >= 18000) {
						const repDiv = document.createElement("div");
						repDiv.classList.add("clear-formatting");

						const repPrice = Math.clamp(price(true) / 2, 0, 18000);
						repDiv.append("You have so much political capital that you can spend reputation to acquire ownership by spending reputation. ",
							App.Arcology.getCellLink("Use reputation", that.name, () => that.cellPassage(containingBuilding),
								() => {
									repX(-(repPrice), "capEx");
									A.demandFactor += DEMAND_FACTOR_DIFF;
									cell.owner = 1;
									App.Arcology.updateOwnership();
								}));
						fragment.append(repDiv);
					}
				} else {
					fragment.append("Too much of the arcology is owned by a single minority holder for you to force a purchase of this sector right now. Your control of the arcology should naturally resolve this situation in a few weeks.");
				}
			}
			return fragment;
		}
	}

	/**
	 * @returns {string|Node}
	 * @protected
	 */
	_setting() {
		return "baseCell";
	}

	/**
	 * @param {App.Arcology.Building} containingBuilding
	 * @returns {Node}
	 * @protected
	 */
	_body(containingBuilding) {
		return document.createDocumentFragment();
	}

	/**
	 * @param {string} name
	 * @param {function(): void} action
	 * @param {number} cost
	 * @param {App.Arcology.Building} containingBuilding
	 * @param {string[]} [notes]
	 * @returns {HTMLDivElement}
	 * @protected
	 */
	_makeInternalUpgrade(name, action, cost, containingBuilding, notes) {
		return makePurchase(name, cost, "capEx", {
			notes,
			handler: () => {
				if (Dialog.isOpen()) {
					Dialog.close();
				}
				if (action) {
					action();
					V.building.refresh();
				}
				Dialog.create(this.name);
				$(Dialog.body()).empty().append(this.cellPassage(containingBuilding));
				Dialog.open();
			},
		});
	}

	/**
	 * @param {string} name
	 * @param {function(): void} action
	 * @param {number} cost
	 * @param {string} passage
	 * @param {string[]} [notes]
	 * @returns {HTMLDivElement}
	 * @protected
	 */
	_makeExternalUpgrade(name, action, cost, passage, notes) {
		const div = document.createElement("div");

		div.append(makePurchase(name, cost, "capEx", {
			notes,
			handler: () => {
				action();
				Dialog.close();
				Engine.play(passage);
			}
		}));

		return div;
	}

	/**
	 * @returns {boolean}
	 */
	canBeSold() {
		return false;
	}

	static _cleanupConfigScheme(config) {
		super._cleanupConfigScheme(config);
		// BC code
	}

	/** @returns {App.Arcology.Cell.BaseCell} */
	clone() {
		return new App.Arcology.Cell.BaseCell(this.owner)._init(this);
	}

	get className() { return "App.Arcology.Cell.BaseCell"; }
};


code:
/** base class for class-based events */
App.Events.BaseEvent = class BaseEvent {
	/**
	 * build a new event
	 * parameters are necessary for serialization (so that saving with the event active will work correctly) and should not normally be used directly
	 * child classes will inherit this implementation automatically, and should not normally need their own constructor implementation
	 * @param {Array<number>} [actors]
	 * @param {object} [params]
	 */
	constructor(actors, params) {
		/** @member {Array<number>} actors - a list of IDs for the actors participating in this event. */
		this.actors = actors || [];
		/** @member {object} params - a set of parameters to pass to the event. */
		this.params = params || {};
	}

	/** Event predicates determine whether the event will occur or not.
	 * @callback eventPredicate
	 * @returns {boolean}
	 */
	/** generate an array of zero or more predicates which must all return true in order for the event to be valid.
	 * lambda predicates may add properties to {@link App.Events.BaseEvent#params the params member} in order to pass information on to the event.
	 * child classes should implement this.
	 * @returns {Array<eventPredicate>}
	 */
	eventPrerequisites() {
		return [];
	}

	/** Actor predicates determine whether an actor is qualified for an event or not.
	 * @callback actorPredicate
	 * @param {FC.SlaveState} slave
	 * @returns {boolean}
	 */
	/** generate an array of zero or more arrays, each corresponding to an actor in the event, which contain zero or more predicates which must be satisfied by the actor.
	 * child classes should implement this, unless they don't need any actors, or are overriding castActors.
	 * @returns {Array<Array<actorPredicate>>}
	 */
	actorPrerequisites() {
		return [];
	}

	/** get the choice weight for the event. Weights should be integral; higher weight means higher probability of selection.
	 * @returns {number}
	 */
	get weight() {
		return 1;
	}

	/** run the event and attach DOM output to the event passage.
	 * child classes must implement this.
	 * @param {ParentNode} node - Document fragment which event output should be attached to
	 */
	execute(node) {
	}

	/** clone the event (needed for serialization).
	 * default implementation should suffice for child classes */
	clone() {
		return _.cloneDeep(this);
	}

	/** serialize the event instance so it persists through saves.
	 * default implementation should suffice for child classes assigned to App.Events */
	toJSON() {
		const reviveData = {actors: this.actors, params: this.params};
		return Serial.createReviver(`new App.Events.${this.constructor.name}($ReviveData$.actors, $ReviveData$.params)`, reviveData);
	}

	/** get the event's (human-readable) name. must not include ":".
	 * default implementation should generally suffice (though we might want to de-camelcase here or something).
	 * @returns {string}
	 */
	get eventName() {
		return this.constructor.name;
	}

	/** build the actual list of actors that will be involved in this event.
	 * default implementation should suffice for child classes with a fixed number of actors; may be overridden for events with variable actor count.
	 * @param {FC.SlaveState} [firstActor] - if supplied, the first actor should be this slave (fail if she is not qualified)
	 * @returns {boolean} - return false if sufficient qualified actors could not be found (cancel the event)
	 */
	castActors(firstActor) {
		const prereqs = this.actorPrerequisites();

		// if casting is already complete and still valid, don't do it again
		if (this.actors.length === prereqs.length) {
			let recast = false;
			for (let i = 0; i < this.actors.length; ++i) {
				const actor = getSlave(this.actors[i]);
				recast = actor && prereqs[i].some(p => !p(actor));
			}
			if (!recast) {
				return true; // all roles previously cast & prereqs still met
			}
		}

		this.actors = [];

		let i = 0;
		if (firstActor && prereqs.length > 0) {
			if (prereqs[0].every(p => p(firstActor))) {
				this.actors.push(firstActor.ID);
			} else {
				return false; // preselected first actor was not qualified
			}
			i = 1; // first actor is cast
		}

		for (; i < prereqs.length; ++i) {
			const qualified = getSlaves().filter(s => !this.actors.includes(s.ID) && prereqs[i].every(p => p(s)));
			if (qualified.size === 0) {
				return false; // a required actor was not found
			}
			this.actors.push(qualified.pluck().key);
		}

		return true; // all actors cast
	}
};

/** This is a trivial event for use as an example. */
App.Events.TestEvent = class TestEvent extends App.Events.BaseEvent {
	constructor(actors, params) {
		super(actors, params);
	}

	eventPrerequisites() {
		return [];
	}

	actorPrerequisites() {
		return [
			[] // one actor, no requirements
		];
	}

	execute(node) {
		let [eventSlave] = this.actors.map(a => getSlave(a)); // mapped deconstruction of actors into local slave variables
		App.Events.drawEventArt(node, eventSlave);
		node.appendChild(document.createTextNode(`This test event for ${eventSlave.slaveName} was successful.`));
	}
};


code:
App.Facilities.Pit.Fights.BaseFight = class BaseFight {
	/**
	 * build a new fight
	 */
	constructor() {
		/** @member {Array<number>} actors - a list of IDs for the actors participating in this fight. */
		this.actors = [];
		/** @member {object} params - a set of parameters to pass to the fight. */
		this.params = {};
	}

	/** A unique key, so we can queue the fight.
	 * @returns {string}
	 */
	get key() {
		return "base fight";
	}

	/**
	 * Whether slaves training at the arena are allowed in this fight. If yes, they will be preferred.
	 * @returns {boolean}
	 */
	get allowTrainees() {
		return false;
	}

	/**
	 * At least one slave is expected to die.
	 * @returns {boolean}
	 */
	get lethal() {
		return false;
	}

	/**
	 * How high the impact of this fight on the total event is. A flat multiplier. Used in descriptions.
	 * 1 is a nonlethal 1-vs-1 fight. May not be negative
	 * @returns {number}
	 */
	get impact() {
		return 1;
	}

	/** Get a short description to show when selecting the fight during the event.
	 * Assumes fight can be run and actors have been cast already
	 * @returns {DocumentFragment}
	 */
	fightDescription() {
		return new DocumentFragment();
	}

	/** Fight predicates determine whether the fight can be shown/executed
	 * @callback pitFightPredicate
	 * @returns {boolean}
	 */
	/** generate an array of zero or more predicates which must all return true in order for the fight to be valid.
	 * lambda predicates may add properties to {@link App.Facilities.Pit.Fights.BaseFight#params the params member} in order to pass information on to the fight.
	 * child classes should implement this.
	 * @returns {Array<pitFightPredicate>}
	 */
	fightPrerequisites() {
		return [
			() =>
				(V.pit.lethal === 0 && !this.lethal) ||
				(V.pit.lethal === 1) ||
				(V.pit.lethal === 2 && this.lethal)
		];
	}

	/**
	 * Actors that are forced to be a specific slave
	 * @returns {Array<number>}
	 */
	forcedActors() {
		return [];
	}

	/** generate an array of zero or more arrays, each corresponding to an actor in the fight, which contain zero or more predicates which must be satisfied by the actor.
	 * child classes should implement this, unless they are overriding castActors.
	 * @returns {Array<Array<actorPredicate>>}
	 */
	actorPrerequisites() {
		return [];
	}

	/** run the fight and attach DOM output to the pit fight passage.
	 * child classes must implement this.
	 * @param {ParentNode} node - Document fragment which fight output should be attached to
	 * @param {App.Facilities.Pit.Fights.FighterMap} fighterMap
	 * @returns {number} - How successful the fight was for prestige/cash.
	 *          The expected value should be between -1 and 1 inclusive. May go more extreme for unexpected outcomes.
	 *          At least one maximum/minimum value should be used, and then scaled to other events with the
	 *          {@link App.Facilities.Pit.Fights.BaseFight#impact impact property}.
	 */
	execute(node, fighterMap) {
		return 0;
	}

	/** build the actual list of actors that will be involved in this fight.
	 * default implementation should suffice for child classes with a fixed number of actors; may be overridden for fights with variable actor count.
	 * @param {{ firstFighter: FC.HumanID; secondFighter: FC.HumanID; }} [toFight={firstFighter: undefined, secondFighter: undefined}]
	 * @returns {boolean} - return false if sufficient qualified actors could not be found (cancel the fight)
	 */
	castActors(toFight = {firstFighter: -1, secondFighter: -1}) {
		const prereqs = this.actorPrerequisites();

		this.actors = [...this.forcedActors()];
		switch (this.actors.length) {
			// add both custom fighters if they're not invalid
			case 0:
				if (toFight.firstFighter >= 0) {
					this.actors.push(toFight.firstFighter);
				}
				if (toFight.secondFighter >= 0) {
					this.actors.push(toFight.secondFighter);
				}
				break;
			// add either fighter, defaulting to first
			case 1:
				if (toFight.firstFighter >= 0) {
					this.actors.push(toFight.firstFighter);
				} else if (toFight.secondFighter >= 0) {
					this.actors.push(toFight.secondFighter);
				}
				break;
		}
		// if not fully chosen, select until we have 2 actors
		for (let i = 0; i < prereqs.length; ++i) {
			if (this.actors.length >= 2) {
				continue;
			}
			if (this.allowTrainees) {
				if (this._selectActor(prereqs[i], ...App.Entity.facilities.pit.job("trainee").employeesIDs())) {
					continue;
				}
			}
			if (!this._selectActor(prereqs[i], ...App.Entity.facilities.pit.job("fighter").employeesIDs())) {
				return false;
			}
		}
		return true; // all actors cast
	}

	/**
	 * @param {Array<actorPredicate>} prereqs
	 * @param {...FC.HumanID} ids
	 * @returns {boolean} False, if no actor could be selected
	 * @private
	 */
	_selectActor(prereqs, ...ids) {
		const qualified = ids
			.filter(si => !this.actors.includes(si) && prereqs.every(p => p(getSlave(si))) && this._validActor(si));
		if (qualified.length === 0) {
			return false; // a required actor was not found
		}
		this.actors.push(qualified.pluck());
		return true;
	}

	/**
	 * @param {FC.HumanID} si - Slave ID
	 * @returns {boolean} True, if the slave is allowed to fight
	 * @private
	 */
	_validActor(si) {
		if (!V.pit.minimumHealth) {
			return true;
		}
		const slave = getSlave(si);
		return canWalk(slave) && slave.health.condition >= -20;
	}
};

/** This is a trivial fight for use as an example. */
App.Facilities.Pit.Fights.TestFight = class extends App.Facilities.Pit.Fights.BaseFight {
	get key() {
		return "test";
	}

	actorPrerequisites() {
		return [
			[], // actor one, no requirements
			[] // actor two, no requirements
		];
	}

	execute(node) {
		let [slave1, slave2] = this.actors.map(a => getSlave(a)); // mapped deconstruction of actors into local slave variables
		node.appendChild(document.createTextNode(`This test fight for ${slave1.slaveName} and ${slave2.slaveName} was successful.`));
		return 0;
	}
};

App.Facilities.Pit.Fights.FighterMap = class {
	constructor() {
		/**
		 * @type {Map<number, number>}
		 */
		this.map = new Map();
	}

	fightCount(slaveId) {
		const count = this.map.get(slaveId);
		if (count === undefined) {
			return 0;
		} else {
			return count;
		}
	}

	addFight(slaveId) {
		const oldCount = this.fightCount(slaveId);
		this.map.set(slaveId, oldCount + 1);
	}

	fighterCount() {
		return this.map.size;
	}

	fightsCount() {
		let count = 0;
		this.map.forEach((v, k) => {
			count += v;
		});
		return count;
	}
};


code:
/** base class for prompt parts */
App.Art.GenAI.PromptPart = class PromptPart {
	/**
	 * @param {FC.HumanState} slave
	 */
	constructor(slave) {
		this.slave = slave;
		this.censored = this.slave.visualAge < 18 && V.aiAgeFilter;
		this.helper = App.Art.GenAI.PromptHelpers;
		this.isFeminine = App.Art.GenAI.PromptHelpers.isFeminine(slave);
		this.isMasculine = App.Art.GenAI.PromptHelpers.isMasculine(slave);
	}

	/**
	 * Define any relevant keywords for the positive prompt
	 * @returns {string|undefined}
	 * @abstract
	 */
	positive() {
		return undefined;
	}

	/**
	 * Define any relevant keywords for the negative prompt
	 * @returns {string|undefined}
	 * @abstract
	 */
	negative() {
		return undefined;
	}

	/**
	 * Keywords for a high detail pass -- useful once the first pass has established image structure
	 * @returns {string|undefined}
	 * @abstract
	 */
	detailer() {
		return undefined; // hopefully just returns an empty prompt if nothing is specified here...
	}

	/**
	 * Facial features -- separated for adetailer face pass
	 * @returns {string|undefined}
	 * @abstract
	 */
	face() {
		return undefined;
	}
};

App.Art.GenAI.Prompt = class Prompt {
	/**
	 * @param {App.Art.GenAI.PromptPart[]} parts
	 */
	constructor(parts) {
		this.parts = parts;
	}

	/**
	 * @returns {string}
	 */
	positive() {
		let parts = this.parts.map(part => part.positive() || "").filter(part => part);
		return parts.length ? parts.join(", ") : "";
	}

	/**
	 * @returns {string}
	 */
	negative() {
		let parts = this.parts.map(part => part.negative() || "").filter(part => part);
		return parts.length ? parts.join(", ") : "";
	}

	/**
	 * @returns {string}
	 */
	detailer() {
		let parts = this.parts.map(part => part.detailer() || "").filter(part => part);
		return parts.length ? parts.join(", ") : "";
	}

	/**
	 * @returns {string}
	 */
	face() {
		let parts = this.parts.map(part => part.face() || "").filter(part => part);
		return parts.length ? parts.join(", ") : "";
	}
};


code:
/**
 * A pure DOM Passage, the SugarCube Wikifier never gets invoked.
 */
// @ts-ignore
App.DomPassage = class extends Passage {
	/**
	 * @param {string} title
	 * @param {function():Node} callback
	 * @param {string[]} tags
	 */
	constructor(title, callback, tags = []) {
		super(title, {
			hasAttribute: a => a === "tags",
			getAttribute: () => tags.join(" ")
		});
		this.callback = callback;

		// @ts-ignore
		Story.addDirect(this);
	}

	/**
	 * @returns {Node}
	 */
	render() {
		// In case the callback fails give out a nice error message instead of breaking completely.
		try {
			return this.callback();
		} catch (ex) {
			return App.UI.DOM.formatException(ex);
		}
	}
};


code:
// Articles map
/**
 * @typedef {object} article
 * @property {function():(HTMLElement|DocumentFragment)} render
 * @property {string} [category]
 */
/**
 * @type {Map<string, article>}
 */
App.Encyclopedia.articles = new Map();

/**
 * Adds an article if it does not exist yet.
 * @param {string} key
 * @param {function():(HTMLElement|DocumentFragment)} article
 * @param {string} [category]
 */
App.Encyclopedia.addArticle = function(key, article, category) {
	if (!App.Encyclopedia.articles.has(key)) {
		App.Encyclopedia.articles.set(key, {render: article, category: category});
	} else {
		console.log(`Trying to overwrite encyclopedia article "${article}".`);
	}
};

/**
 * Renders the specified article.
 * @param {string} article
 * @returns {string|HTMLElement|DocumentFragment}
 */
App.Encyclopedia.renderArticle = function(article) {
	if (App.Encyclopedia.articles.has(article)) {
		return App.Encyclopedia.articles.get(article).render();
	}
	return `Encyclopedia article "${article}" not found.`;
};

// Categories map
/**
 * @type {Map<string, (currentArticle:string)=>(Node)>}
 */
App.Encyclopedia.categories = new Map();

/**
 * Adds an article if it does not exist yet.
 * @param {string} key
 * @param {(currentArticle:string)=>(Node)} category
 */
App.Encyclopedia.addCategory = function(key, category) {
	if (!App.Encyclopedia.categories.has(key)) {
		App.Encyclopedia.categories.set(key, category);
	} else {
		console.log(`Trying to overwrite encyclopedia category "${category}".`);
	}
};

/**
 * Renders the specified article.
 * @param {string} article
 * @returns {string|Node}
 */
App.Encyclopedia.renderCategory = function(article) {
	if (!App.Encyclopedia.articles.has(article)) {
		return `Encyclopedia article "${article}" not found.`;
	}

	const category = App.Encyclopedia.articles.get(article).category;
	if (!category) {
		return `Encyclopedia article "${article}" has no category.`;
	}
	if (!App.Encyclopedia.categories.has(category)) {
		return `Encyclopedia category "${category}" not found.`;
	}
	return App.Encyclopedia.categories.get(category)(article);
};

// Utility functions
App.Encyclopedia.topic = function(topic) {
	return App.UI.DOM.makeElement("span", topic, ["encyclopedia", "topic"]);
};

/**
 * @param {Node} parent
 * @param {string} source
 * @param {string} [author]
 */
App.Encyclopedia.addArticleSource = function(parent, source, author) {
	const r = [];
	if (author) {
		r.push(`<span class='author'>${author},</span>`);
	}
	r.push(`<span class='article'>${source}</span>`);
	App.Events.addNode(parent, r, "p", "encyclopedia-source");
};


code:
App.Data.FacilityDesc = class {
	constructor() {
		/** Base name for state variables */
		this.baseName = "";
		/** Generic name for UI (Brothel, Club, etc.)
		 * If null, baseName is used instead
		 */
		this.genericName = "";
		/** @type {{[key: string]: FC.Data.JobDesc}} */
		this.jobs = {};
		this.defaultJob = "";
		/** @type {FC.Data.ManagerJobDesc} */
		this.manager = null;
		this.decorated = false;
	}
};

/** @type {{[key: string]: FC.FacilityFramework}} */
App.Data.Facilities = {};
App.Entity.Facilities = {};

App.Entity.Facilities.Job = class {
	constructor() {
		/** @type {FC.Data.JobDesc} */
		this.desc = null;
		/** @type {App.Entity.Facilities.Facility} */
		this.facility = null;
	}

	/**
	 * Does slave meet the requirements for this job
	 * @param {FC.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = [];
		if (this.desc.publicSexUse &&
			(slave.breedingMark === 1 && V.propOutcome === 1 && V.eugenicsFullControl !== 1 && FutureSocieties.isActive('FSRestart'))) {
			r.push(`${slave.slaveName} is for private use only.`);
		}
		if (!this.desc.fuckdollAccepted && slave.fuckdoll > 0) {
			r.push(`Fuckdolls can't ${this.desc.assignment} at ${this.facility.name}.`);
		}
		if (!this.desc.broodmotherAccepted && slave.preg >= 36 && slave.broodmother === 2) {
			r.push(`Birthing broodmothers can't ${this.desc.assignment}.`);
		}
		return r;
	}

	/**
	 * Can slave be employed at this position
	 * @param {FC.SlaveState} slave
	 * @returns {string[]}
	 */
	canEmploy(slave) {
		let r = [];
		if (slave.assignment === this.desc.assignment) {
			r.push(`${slave.slaveName} is already assigned to ${this.desc.assignment}.`);
		} else if (this.isEmployed(slave)) {
			r.push(`${slave.slaveName} is already assigned to ${this.desc.assignment} at ${this.facility.name}.`);
		} else if (!this._facilityHasFreeSpace) {
			r.push(`Capacity of ${this.facility.name} exceeded.`);
		}
		r.push(...this.checkRequirements(slave));
		return r;
	}

	/**
	 * Is slave with the given ID already assigned to this job
	 * @param {number} id
	 * @returns {boolean}
	 */
	hasEmployeeWithId(id) {
		return this.employeesIDs().has(id);
	}

	/**
	 * Is slave already assigned to this job
	 * @param {FC.SlaveState} slave
	 * @returns {boolean}
	 */
	isEmployed(slave) {
		return this.hasEmployeeWithId(slave.ID);
	}

	/**
	 * @callback assignmentCallback
	 * @param {FC.SlaveState} slave the slave whose assignment changes
	 * @param {FC.Assignment} assignment new assignment
	 * @returns {void}
	 */

	/**
	 * Returns link text for the penthouse assignment
	 * @param {number} ID slave ID
	 * @param {string} [passage] passage to go to
	 * @param {assignmentCallback} [callback]
	 * @param {string} [linkText]
	 * @returns {HTMLAnchorElement}
	 */
	assignmentLinkElement(ID, passage, callback, linkText) {
		linkText = linkText || this.desc.position;
		return App.UI.DOM.assignmentLink(getSlave(ID), this.desc.assignment, passage, callback, linkText);
	}

	/**
	 * all slaves that are employed at this job
	 * @returns {FC.SlaveState[]}
	 */
	employees() {
		return [...this.employeesIDs()].map(id => getSlave(id, true));
	}

	/**
	 * IDs for all slaves that are employed at this job
	 * @returns {Set<number>}
	 */
	employeesIDs() {
		return V.JobIDMap[this.desc.assignment];
	}

	/**
	 * assignment string but the generic facility name replaced with its in-game name
	 */
	get assignment() {
		return this.desc.assignment.replace("the " + this.facility.desc.baseName, this.facility.name);
	}

	/**
	 * Tests if slave is broken enough
	 * @protected
	 * @param {FC.SlaveState} slave
	 * @param {number} [pureDevotion=50] Minimal devotion level to pass test with any trust
	 * @param {number} [devotion=-50] Minimal devotion for slaves with enough fear
	 * @param {number} [trust=-21] Maximal trust (i.e. minimal fear) for the less devotional (see above)
	 * @param {number} [pureFear=-51] Maximal low trust to pass test with any devotion (because of the fear)
	 * @param {number} [pureTrust=101] Minimal high trust level to pass test without devotion
	 * @returns {boolean}
	 */
	static _isBrokenEnough(slave, pureDevotion = 50, devotion = -51, trust = -21, pureFear = -51, pureTrust = 101) {
		return slave.devotion >= pureDevotion ||
			slave.trust >= pureTrust ||
			slave.trust <= pureFear ||
			(
				slave.devotion > devotion &&
				slave.trust < trust
			);
	}

	/**
	 * @protected
	 * Standard message that slave is not broken enough
	 * @param {FC.SlaveState} slave
	 * @returns {string}
	 */
	static _stdBreakageMessage(slave) {
		return `${slave.slaveName} must be either more fearful of you or devoted to you.`;
	}

	/** @protected */
	get _facilityHasFreeSpace() {
		return this.facility.hasFreeSpace;
	}
};

App.Entity.Facilities.ManagingJob = class extends App.Entity.Facilities.Job {
	constructor() {
		super();
		/** @type {FC.Data.ManagerJobDesc} */
		this.desc = null;
	}

	/**
	 * Can slave be employed at this position
	 * @param {FC.SlaveState} slave
	 * @returns {string[]}
	 */
	checkRequirements(slave) {
		let r = super.checkRequirements(slave);
		if (slave.devotion < this.desc.requiredDevotion) {
			r.push(`${slave.slaveName} must be more devoted to you.`);
		}
		if (this.desc.shouldWalk && !canWalk(slave)) {
			r.push(`${slave.slaveName} must be able to walk.`);
		}
		if (this.desc.shouldHold && !canHold(slave)) {
			r.push(`${slave.slaveName} must be able to hold on to objects.`);
		}
		if (this.desc.shouldSee && !canSee(slave)) {
			r.push(`${slave.slaveName} must have working eyes.`);
		}
		if (this.desc.shouldHear && !canHear(slave)) {
			r.push(`${slave.slaveName} must be able to hear.`);
		}
		if (this.desc.shouldTalk && !canTalk(slave)) {
			r.push(`${slave.slaveName} must be able to talk.`);
		}
		if (this.desc.shouldThink && slave.fetish === Fetish.MINDBROKEN) {
			r.push(`${slave.slaveName} must possess cognition.`);
		}
		return r;
	}

	/**
	 * Returns true if slave has enough applicable skill or career
	 * @param {FC.SlaveState} slave
	 * @returns {boolean}
	 */
	slaveHasExperience(slave) {
		return (this.desc.skill && slave.skill[this.desc.skill] >= Constant.MASTERED_XP) || (this.desc.careers.includes(slave.career));
	}

	/** @returns {FC.SlaveState} */
	get currentEmployee() {
		const employees = this.employees();
		return employees.length > 0 ? employees[0] : null;
	}

	/** @protected */
	get _facilityHasFreeSpace() {
		return true;
	}

	/** @returns {string} */
	get positionAbbreviation() {
		return this.desc.positionAbbreviation || "L";
	}
};

App.Entity.Facilities.Facility = class {
	/**
	 * @param {FC.Data.FacilityDesc} desc defines state variable for this facility
	 * @param {{[key: string]: App.Entity.Facilities.Job}} [jobs] job object that are not default
	 * @param {App.Entity.Facilities.ManagingJob} [manager]
	 */
	constructor(desc, jobs, manager) {
		this.desc = desc;
		jobs = jobs || {};
		/** @private @type {{[key: string]: App.Entity.Facilities.Job}} */
		this._jobs = {};

		for (const jn in this.desc.jobs) {
			if (jobs[jn] !== undefined) {
				this._jobs[jn] = jobs[jn];
			} else {
				this._jobs[jn] = this._createJob(jn);
			}
			this._jobs[jn].facility = this;
			this._jobs[jn].desc = desc.jobs[jn];
		}

		if (manager === undefined) {
			// default manager job implementation
			manager = (this.desc.manager !== null) ? new App.Entity.Facilities.ManagingJob() : null;
		}
		/**
		 * @private
		 * @type {App.Entity.Facilities.ManagingJob}
		 */
		this._manager = manager;
		if (this._manager !== null) {
			this._manager.facility = this;
			this._manager.desc = this.desc.manager;
		}
	}

	/** Facility display name
	 * @returns {string} */
	get name() {
		const stateObj = V[this.desc.baseName];
		const res = (stateObj && typeof stateObj === "object") ? stateObj.name : V[this.desc.baseName + "Name"];
		return res !== undefined ? res : 'the ' + this.genericName;
	}

	set name(newName) {
		const stateObj = V[this.desc.baseName];
		if (stateObj && typeof stateObj === "object") {
			stateObj.name = newName;
		} else {
			V[this.desc.baseName + "Name"] = newName;
		}
	}

	/** @returns {string} */
	get nameCaps() {
		return capFirstChar(this.name);
	}

	/** @returns {string} */
	get UIName() {
		return capFirstChar(this.genericName);
	}

	/** Facility generic name ("Brothel", "Schoolroom", etc.)
	 * @returns {string} */
	get genericName() {
		return this.desc.genericName !== null ? this.desc.genericName : capFirstChar(this.desc.baseName);
	}

	/** All jobs at this facility
	 * @returns {string[]}
	 */
	get jobsNames() {
		return Object.keys(this.desc.jobs);
	}

	/**
	 * Returns job description
	 * @param {string} [name] job name; the default job will be used if omitted
	 * @returns {App.Entity.Facilities.Job}
	 */
	job(name) {
		return this._jobs[name || this.desc.defaultJob];
	}

	get jobs() {
		return Object.values(this._jobs);
	}

	get manager() {
		return this._manager;
	}

	/**
	 * Facility slave capacity
	 * @returns {number} */
	get capacity() {
		return V[this.desc.baseName] !== null && ((typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName].capacity : V[this.desc.baseName]);
	}

	/**
	 *
	 * @param {number} capacity
	 */
	set capacity(capacity) {
		const stateObj = V[this.desc.baseName];
		if (stateObj && typeof stateObj === "object") {
			stateObj.capacity = capacity;
		} else {
			V[this.desc.baseName] = capacity;
		}
	}

	get established() {
		return this.capacity > 0;
	}

	/** Number of already hosted slaves
	 * @param {string} [jobName]
	 * @returns {number} */
	hostedSlaves(jobName) {
		if (jobName) {
			return this._jobs[jobName].employeesIDs().size;
		}
		return this.jobs.reduce((acc, job) => { return acc + job.employeesIDs().size; }, 0);
	}

	/**
	 * Count of employees plus manager altogether
	 * @returns {number}
	 */
	get totalEmployeesCount() {
		return this.hostedSlaves() + ((this.manager && this.manager.currentEmployee) ? 1 : 0);
	}

	/**
	 * @returns {number} count of non slave occupants (e.g. bioreactors in the dairy)
	 */
	get nonEmployeeOccupantsCount() {
		return 0;
	}

	/**
	 * @returns {number} total count of facility places taken by employees and other beings (e.g. bioreactors in the dairy)
	 */
	get totalOccupants() {
		return this.hostedSlaves() + this.nonEmployeeOccupantsCount;
	}

	get hasEmployees() {
		return this.jobs.some(j => j.employeesIDs().size > 0);
	}

	get hasFreeSpace() {
		return this.capacity > this.totalOccupants;
	}

	get freeSpace() {
		return this.capacity > 0 ? this.capacity - this.totalOccupants : 0;
	}

	/**
	 * @param {string} name
	 * @returns {number}
	 */
	option(name) {
		return (typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName][name] : V[this.desc.baseName + name];
	}

	/**
	 *
	 * @param {string} name
	 * @returns {number}
	 */
	rule(name) {
		return (typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName].rules[name] : V[name];
	}

	/**
	 * Sets a rule value. Only use this on a facility migrated to an object in V.
	 * @param {string} name
	 * @param {number} value
	 */
	setRule(name, value) {
		V[this.desc.baseName].rules[name] = value;
	}

	/**
	 * @param {string} name
	 * @returns {number}
	 */
	upgrade(name) {
		return (typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName].upgrades[name] : this.option("Upgrade" + name);
	}

	/**
	 * Sets an upgrade value. Only use this on a facility migrated to an object in V.
	 * @param {string} name
	 * @param {number} value
	 */
	setUpgrade(name, value) {
		V[this.desc.baseName].upgrades[name] = value;
	}

	/** Can this facility be decorated? */
	get isDecorated() {
		return this.desc.decorated;
	}

	/** How is this facility currently decorated?
	 * @returns {FC.FutureSocietyDeco}
	 */
	get decoration() {
		if (!this.isDecorated) {
			return "standard";
		}
		return (typeof V[this.desc.baseName] === "object") ? V[this.desc.baseName].decoration : V[this.desc.baseName + "Decoration"];
	}

	/** Configure facility decoration
	 * @param {FC.FutureSocietyDeco} val
	 */
	set decoration(val) {
		if (this.isDecorated) {
			if (typeof V[this.desc.baseName] === "object") {
				V[this.desc.baseName].decoration = val;
			} else {
				V[this.desc.baseName + "Decoration"] = val;
			}
		}
	}

	/** Returns the facility's value, including all purchased upgrades. */
	get value() {
		return 10000;
	}

	/**
	 * Whether the facility is located within the penthouse or in the broader arcology
	 * @returns {boolean}
	 */
	get isInPenthouse() {
		return this.penthouseFacility;
	}

	set isInPenthouse(val) {
		this.penthouseFacility = val;
	}

	/**
	 * Can this facility host the given slave
	 * @param {FC.SlaveState} slave
	 * @param {string} [job] default job if omitted
	 * @returns {string[]} array with rejection reasons. Slave can be hosted if this is empty.
	 */
	canHostSlave(slave, job) {
		const j = this.job(job);
		if (j === undefined) {
			if (this.jobs.length === 0) {
				const err = `${this.genericName} facility has no jobs for slaves`;
				console.warn(err);
				return [err];
			} else {
				console.warn(`Can't find job ${job} at ${this.name}.`); // eslint-disable-line no-console
			}
		}
		return j.canEmploy(slave);
	}

	/**
	 * Does the given slave work at this facility
	 * @param {FC.SlaveState} slave
	 * @returns {boolean}
	 */
	isHosted(slave) {
		return this.jobs.some(job => job.isEmployed(slave));
	}

	/**
	 * Returns link text for the job assignments
	 * @param {number} ID slave ID
	 * @param {string} [job] generate link only for this job
	 * @param {string} [passage]
	 * @param {assignmentCallback} [callback]
	 * @returns {HTMLElement[]}
	 */
	assignmentLinkElements(ID, job, passage, callback) {
		/** @type {FC.SlaveState} */
		const slave = getSlave(ID);
		const jobs = job === undefined ? this._jobs : {job: this._jobs[job]};

		let res = [];
		for (const jn in jobs) {
			const j = jobs[jn];
			let rejects = j.canEmploy(slave);
			if (rejects.length === 0) {
				res.push(j.assignmentLinkElement(ID, passage, callback));
			} else {
				res.push(App.UI.DOM.disabledLink(j.desc.position, rejects));
			}
		}
		return res;
	}

	/**
	 * Returns link text for the facility transfer
	 * @param {number} ID slave ID
	 * @param {string} [job] transfer to this job (uses default job if this is undefined)
	 * @param {string} [passage]
	 * @param {assignmentCallback} [callback]
	 * @returns {HTMLElement}
	 */
	transferLinkElement(ID, job, passage, callback) {
		job = job || this.desc.defaultJob;
		return this._jobs[job].assignmentLinkElement(ID, passage, callback, this.genericName);
	}

	/**
	 * all slaves that are employed at this job
	 * @returns {FC.SlaveState[]}
	 */
	employees() {
		const jobArray = this.jobs;
		if (jobArray.length === 1) {
			return jobArray[0].employees();
		}
		return [].concat(...jobArray.map(j => j.employees()));
	}

	/**
	 * IDs for all slaves that are employed at this facility (excluding the manager)
	 * @returns {Set<number>}
	 */
	employeesIDs() {
		const jobArray = this.jobs;
		if (jobArray.length === 1) {
			return jobArray[0].employeesIDs();
		}
		const res = new Set();
		for (const j of jobArray) {
			const ids = j.employeesIDs();
			for (const id of ids) {
				res.add(id);
			}
		}
		return res;
	}

	/**
	 * @param {boolean} long
	 * @returns {string}
	 */
	occupancyReport(long) {
		if (this.jobs.length === 0) {
			return this.manager && this.manager.currentEmployee ? this.manager.positionAbbreviation : "";
		}
		const nonEmployees = this.nonEmployeeOccupantsCount;
		const managerStr = this.manager && this.manager.currentEmployee ? (
			long ? `, ${this.manager.desc.position}` : `, ${this.manager.positionAbbreviation}`) : "";
		const slavesStr = long ? ` ${this.job().desc.position}${this.capacity !== 1 ? 's' : ''}` : "";
		return `${num(this.hostedSlaves())}${nonEmployees ? `+${num(nonEmployees)}` : ""}/${num(this.capacity)}${slavesStr}${managerStr}`;
	}

	/**
	 * @protected
	 * @param {string} jobName
	 * @returns {App.Entity.Facilities.Job}
	 */
	_createJob(jobName) { /* eslint-disable-line no-unused-vars*/
		return new App.Entity.Facilities.Job();
	}
};

/**
 * Job for a facility with a single job option
 */
App.Entity.Facilities.FacilitySingleJob = class extends App.Entity.Facilities.Job {
	/**
	 * @param {number} ID slave ID
	 * @param {string} [targetPassage] passage to go to
	 * @param {assignmentCallback} [callback]
	 * @param {string} [linkText]
	 * @returns {HTMLAnchorElement}
	 */
	assignmentLinkElement(ID, targetPassage, callback, linkText) {
		linkText = linkText || this.desc.position;
		return App.UI.DOM.assignmentLink(getSlave(ID), this.desc.assignment, "",
			(slave, assignment) => {
				if (callback) {
					callback(slave, assignment);
				}
				assignmentTransition(slave, assignment, targetPassage || passage());
			}, linkText);
	}
};

App.Entity.Facilities.SingleJobFacility = class extends App.Entity.Facilities.Facility {
	/**
	 * @param {FC.Data.FacilityDesc} desc defines state variable for this facility
	 * @param {{[key: string]: App.Entity.Facilities.Job}} [jobs] job object that are not default
	 * @param {App.Entity.Facilities.ManagingJob} [manager]
	 */
	constructor(desc, jobs, manager) {
		super(desc, jobs, manager);
		if (this.jobs.length !== 1) {
			throw Error(`SingleJobFacility accepts only a single job, but ${this.jobs.length} were provided`);
		}
		this._job = this.job(); // cache the only job
	}

	/**
	 * @override
	 * @protected
	 * @returns {App.Entity.Facilities.FacilitySingleJob}
	 */
	_createJob() {
		return new App.Entity.Facilities.FacilitySingleJob();
	}

	get facilityJob() {
		return this._job;
	}
};

/** Instances of all facility objects */
App.Entity.facilities = {};


code:
/**
 * Creates the facility passage for a facility.
 *
 * Not to be confused with `App.Entity.Facilities.Facility`, which handles the logic.
 */
App.Facilities.Facility = class Facility {
	/**
	 * @param {App.Entity.Facilities.Facility} facility The instance form of the facility. Typically found in `App.Entity.facilities`.
	 * @param {function():void} decommissionHandler
	 */
	constructor(facility, decommissionHandler) {
		this.facility = facility;

		/** @private */
		this._div = document.createElement("div");
		/** @private @type {Array<function():HTMLDivElement>} */
		this._sections = [];
		/** @private @type {FC.IUpgrade[]} */
		this._upgrades = [];
		/** @private @type {FC.Facilities.Rule[]} */
		this._rules = [];

		this._addUpgrades(...this.upgrades);
		this._addRules(...this.rules);

		this._addSections(
			() => this._makeIntro(decommissionHandler),
			() => this._makeExpand(),
			() => this._makeUpgrades(),
			() => this._makeRules(),
			() => this._makeCustomNodes(),
			() => this._makeMenials(),
			() => this._makeStats(),
			() => this._makeAds(),
			() => this._makeSlaves(),
			() => this._makeRename(),
		);
	}

	/**
	 * Puts the different sections together into one passage.
	 *
	 * @private
	 * @returns {DocumentFragment}
	 */
	_assemble() {
		const frag = new DocumentFragment();

		this._sections.forEach(section => frag.append(App.UI.DOM.makeElement("div", section(), ['margin-bottom'])));

		return frag;
	}

	/**
	 * Renders the facility on-screen.
	 *
	 * @returns {HTMLDivElement}
	 */
	render() {
		this._div.append(this._assemble());

		return this._div;
	}

	/**
	 * Refreshes the facility on-screen.
	 *
	 * @returns {void}
	 */
	refresh() {
		App.UI.DOM.replace(this._div, this._assemble());
	}

	/**
	 * Adds new sections to the facility passage.
	 *
	 * @param {...function():HTMLDivElement} args
	 *
	 * @private
	 * @returns {void}
	 */
	_addSections(...args) {
		this._sections.push(...args);
	}

	/**
	 * Adds new purchaseable upgrades.
	 * @param {...FC.IUpgrade} upgrades
	 *
	 * @private
	 * @returns {void}
	 */
	_addUpgrades(...upgrades) {
		this._upgrades.push(...upgrades);
	}

	/**
	 * Adds new rules able to be set by the player.
	 * @param {...FC.Facilities.Rule} rules
	 *
	 * @private
	 * @returns {void}
	 */
	_addRules(...rules) {
		this._rules.push(...rules);
	}

	/**
	 * Sets up the intro scene.
	 *
	 * @param {function():void} decommissionHandler
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeIntro(decommissionHandler) {
		const div = document.createElement("div");

		App.UI.DOM.appendNewElement("h1", div, this.facility.nameCaps);
		App.UI.DOM.appendNewElement("div", div, this.intro, ['scene-intro']);

		if (this.facility.totalEmployeesCount === 0) {
			div.append(App.UI.DOM.makeElement("div", App.UI.DOM.passageLink(`Decommission ${this.facility.name}`, "Main", decommissionHandler), ['indent']));
		}

		return div;
	}

	/**
	 * Allows the facility to be expanded.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeExpand() {
		const div = document.createElement("div");
		// TODO: rework once all facilities are objects
		const capacity = typeof V[this.facility.desc.baseName] === 'object'
			? V[this.facility.desc.baseName].capacity
			: V[this.facility.desc.baseName];
		const occupancy = this.facility.hostedSlaves();
		const maximum = this.expand.maximum || Number.MAX_SAFE_INTEGER;
		const amount = this.expand.amount;
		const desc = this.expand.desc ||
			`${this.facility.nameCaps} can support ${this.facility.capacity} slaves. There ${this.facility.hostedSlaves() === 1 ? `is` : `are`} currently ${numberWithPluralOne(this.facility.hostedSlaves(), "slave")} here.`;
		const baseCost = 1000 * V.upgradeMultiplierArcology;
		const cost = this.expand.cost || capacity * baseCost;

		div.append(desc);
		if (!this.expand.unexpandable) {
			if (this.expand.maximum) {
				App.UI.DOM.appendNewElement("div", div, `${this.facility.nameCaps} can support a maximum of ${num(maximum)} slaves.`);
			}
			if (capacity < maximum) {
				if (amount) {
					App.UI.DOM.appendNewElement("div", div,
						makePurchase(`Expand slave capacity.`, cost, "capEx", {
							notes: [`increases the capacity of ${this.facility.name} by ${num(amount)}.`],
							handler: () => { expandFacility(amount, cost); }
						})
					);
				} else {
					const link = document.createElement("span");
					const upgrades = [10, 25, 50, 100];
					const available = maximum ? upgrades.filter(s => maximum - occupancy >= s) : upgrades;
					const options = [];
					for (const expand of available) {
						// cost per level follows an arithmetic series; each level costs `5 * baseCost` MORE than the previous level
						// sum of n terms of an arithmetic series is computed as Sn = (n/2)(2a+(n-1)d), where a is the first term being considered and d is the common difference
						const level = expand / 5;
						const exCost = Math.trunc((level / 2) * (2 * cost + (level - 1) * 5 * baseCost));
						const f = new DocumentFragment();
						f.append(App.UI.DOM.link(`x${expand}`, () => {
							expandFacility(expand, exCost);
						}));
						App.Events.addNode(f, [` (${cashFormatColor(exCost)})`]);
						options.push(f);
					}
					link.append(App.UI.DOM.generateLinksStrip(options));

					const linkArray = [];
					App.UI.DOM.appendNewElement("div", div);
					App.Events.addNode(div, [`Expanding ${this.facility.name} by ${num(5)} slots will cost ${cashFormatColor(Math.trunc(cost))}: `]);
					linkArray.push(App.UI.DOM.link(`x5`, () => {
						expandFacility(5, Math.trunc(cost));
					}));
					if (options.length > 0) {
						linkArray.push(App.UI.DOM.linkReplace(`Additional options`, link));
					}
					div.append(App.UI.DOM.generateLinksStrip(linkArray));
				}
			}
		}

		if (this.facility.totalEmployeesCount > 0) {
			App.UI.DOM.appendNewElement("div", div, this.expand.removeAll || removeFacilityWorkers(this.facility, this.expand.removeManager, this.expand.removeSlave), ['indent']);
		}

		const expandFacility = (num, price) => {
			if (typeof V[this.facility.desc.baseName] === 'object') {		// TODO: change this once all facilities are objects
				V[this.facility.desc.baseName].capacity += num;
			} else {
				V[this.facility.desc.baseName] += num;
			}
			V.PC.skill.engineering += 0.1;
			cashX(-price, "capEx");

			App.UI.reload();
		};

		return div;
	}

	/**
	 * Allows the facility to be upgraded.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeUpgrades() {
		const div = document.createElement("div");

		if (this.upgrades.length > 0) {
			App.UI.DOM.appendNewElement("h2", div, `Upgrades`);

			this._upgrades.forEach(u => {
				const upgrade = new App.Upgrade(u.property, u.tiers, u.object);

				div.append(upgrade.render());
			});
		}

		return div;
	}

	/**
	 * Allows rules to be set up in the facility.
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeRules() {
		const div = document.createElement("div");

		if (this.rules.length > 0 && this.rules.some(rule => rule.prereqs.every(prereq => prereq === true))) {
			App.UI.DOM.appendNewElement("h2", div, `Rules`);

			this._rules.forEach(rule => {
				if (rule.prereqs.every(prereq => prereq === true)) {
					const options = new App.UI.OptionsGroup();
					const option = options.addOption(null, rule.property, rule.object || V);

					rule.options.forEach(o => {
						if (!o.prereqs || o.prereqs.every(prereq => prereq === true)) {
							option.addValue(o.link, o.value);
							if (o.handler) {
								option.addCallback(o.handler);
							}
							if (o.note) {
								option.addComment(o.note);
							}

							if ((rule.object && _.isEqual(rule.object[rule.property], o.value)) ||
								_.isEqual(V[rule.property], o.value)) {
								App.UI.DOM.appendNewElement("div", div, o.text);
							}
						}
					});

					App.UI.DOM.appendNewElement("div", div, options.render(), ['margin-bottom']);
				}

				if (rule.nodes) {
					App.Events.addNode(div, rule.nodes);
				}
			});
		}

		return div;
	}

	/**
	 * Creates any custom nodes and adds them to the facility.
	 *
	 * Custom nodes are always placed between the rules section and the menial slaves section (or where they would be).
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeCustomNodes() {
		const div = document.createElement("div");

		if (this.customNodes) {
			this.customNodes.forEach(node => div.append(node));
		}

		return div;
	}

	/**
	 * Allows menial slaves to be bought, sold, and assigned to the facility.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeMenials() {
		const div = document.createElement("div");

		if (this.menials) {
			App.UI.DOM.appendNewElement("h2", div, `Menials`);
			App.UI.DOM.appendNewElement("div", div, this.menials, ['margin-bottom']);
		}

		return div;
	}

	/**
	 * Displays a table with statistics relating to the facility.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeStats() {
		const div = document.createElement("div");

		if (this.stats) {
			App.UI.DOM.appendNewElement("h2", div, `Statistics`);
			App.UI.DOM.appendNewElement("div", div, this.stats, ['margin-bottom']);
		}

		return div;
	}

	/**
	 * Adds a section for managing advertisements to the facility.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeAds() {
		const div = document.createElement("div");

		if (this.ads) {
			App.UI.DOM.appendNewElement("h2", div, `Advertising`);
			App.UI.DOM.appendNewElement("div", div, this.ads, ['margin-bottom']);
		}

		return div;
	}

	/**
	 * Displays a list of slaves that can be assigned and removed.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeSlaves() {
		const div = document.createElement("div");

		App.UI.DOM.appendNewElement("h2", div, `Slaves`);
		App.UI.DOM.appendNewElement("div", div, this.slaves || App.UI.SlaveList.stdFacilityPage(this.facility, true), ['margin-bottom']);

		App.UI.SlaveList.ScrollPosition.restore();

		return div;
	}

	/**
	 * Adds a textbox allowing the facility to be renamed.
	 *
	 * @private
	 * @returns {HTMLDivElement}
	 */
	_makeRename() {
		const div = document.createElement("div");

		App.UI.DOM.appendNewElement("h2", div, `Rename`);
		App.UI.DOM.appendNewElement("div", div, App.Facilities.rename(this.facility, () => this.refresh()));

		return div;
	}

	// Getters and Setters

	// Getters that returns a nullish type are used for type checking and autocompletion.

	/**
	 * The text displayed in the intro scene.
	 *
	 * @returns {string}
	 */
	get intro() {
		return null;
	}

	/**
	 * The facility description in the intro scene.
	 *
	 * @returns {string}
	 */
	get decorations() {
		return null;
	}

	/**
	 * Adds an option to expand the facility.
	 *
	 * Describes how many slaves are in the facility out of how many are supported by default.
	 *
	 * @returns {FC.Facilities.Expand}
	 */
	get expand() {
		return {};
	}

	/**
	 * Any upgrades available for purchase.
	 *
	 * @returns {FC.IUpgrade[]}
	 */
	get upgrades() {
		return [];
	}

	/**
	 * Any rules able to be set.
	 *
	 * @returns {FC.Facilities.Rule[]}
	 */
	get rules() {
		return [];
	}

	/**
	 * Any menials that are able to be assigned.
	 *
	 * @returns {HTMLDivElement}
	 */
	get menials() {
		return null;
	}

	/**
	 * Any statistics table to display.
	 *
	 * @returns {HTMLDivElement}
	 */
	get stats() {
		return null;
	}

	/**
	 * Any advertisements to display.
	 *
	 * @returns {HTMLDivElement}
	 */
	get ads() {
		return null;
	}

	/**
	 * Any slave selection menu to display.
	 *
	 * Uses the standard list if not defined.
	 *
	 * @returns {HTMLDivElement}
	 */
	get slaves() {
		return null;
	}

	/**
	 * Allows for the addition of any custom nodes to the facility.
	 *
	 * @returns {Array<HTMLDivElement|DocumentFragment>}
	 */
	get customNodes() {
		return [];
	}
};


code:
App.SpecialSlavesProxy = class SpecialSlavesProxy {
	constructor() {
	}

	get Attendant() {
		return getSlave(V.AttendantID);
	}
	get Bodyguard() {
		return getSlave(V.BodyguardID);
	}
	get Concubine() {
		return getSlave(V.ConcubineID);
	}
	get DJ() {
		return getSlave(V.djID);
	}
	get Farmer() {
		return getSlave(V.FarmerID);
	}
	get HeadGirl() {
		return getSlave(V.HeadGirlID);
	}
	get Lurcher() {
		return getSlave(V.LurcherID);
	}
	get Madam() {
		return getSlave(V.MadamID);
	}
	get Matron() {
		return getSlave(V.MatronID);
	}
	get Milkmaid() {
		return getSlave(V.MilkmaidID);
	}
	get Nurse() {
		return getSlave(V.NurseID);
	}
	get Recruiter() {
		return getSlave(V.RecruiterID);
	}
	get Schoolteacher() {
		return getSlave(V.SchoolteacherID);
	}
	get Stewardess() {
		return getSlave(V.StewardessID);
	}
	get Stud() {
		return getSlave(V.StudID);
	}
	get Wardeness() {
		return getSlave(V.WardenessID);
	}
};

globalThis.S = new App.SpecialSlavesProxy();