From ffd3c73800baeb4ddb0238c04d21ac2f0971754e Mon Sep 17 00:00:00 2001
From: Frankly George <54015-franklygeorge@users.noreply.gitgud.io>
Date: Wed, 7 Feb 2024 18:58:20 +0000
Subject: [PATCH] Updated Gulp build script, compile.bat, and compile.sh

---
 .eslintrc.json                                |  10 +
 .gitignore                                    |   3 +
 .gitlab-ci.yml                                |   3 +-
 CONTRIBUTING.md                               |   1 -
 build.config.json                             |  13 +-
 compile-legacy.bat                            |   2 +
 compile-legacy.sh                             | 203 +++++++
 compile.bat                                   | 144 ++++-
 compile.sh                                    | 219 ++------
 compile_debug.bat                             |  11 -
 compile_themes.bat                            |  29 -
 gulpfile.js                                   | 502 ++++++++++++++----
 package.json                                  |  14 +-
 ...e_debug+sanityCheck.bat => sanityCheck.bat |  10 +-
 14 files changed, 810 insertions(+), 354 deletions(-)
 create mode 100644 compile-legacy.bat
 create mode 100755 compile-legacy.sh
 delete mode 100644 compile_debug.bat
 delete mode 100644 compile_themes.bat
 rename compile_debug+sanityCheck.bat => sanityCheck.bat (84%)

diff --git a/.eslintrc.json b/.eslintrc.json
index 9dbbf5be39e..0306fb66558 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -31,6 +31,16 @@
 			"impliedStrict": true
 		}
 	},
+	"overrides": [
+		{
+			// overrides for helper scripts that are written
+			// using imports and/or exports
+			"files": ["gulpfile.js"],
+			"parserOptions": {
+				"sourceType": "module"
+			}
+		}
+	],
 	"settings": {
 		"jsdoc": {
 			"mode": "typescript"
diff --git a/.gitignore b/.gitignore
index 9ca9221b11e..12190558f14 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,3 +75,6 @@ src/002-config/fc-version.js.commitHash.js
 # legacy
 devNotes/legacy files/*
 /.vs
+
+# mod development
+/mods/dev/
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 621997da4f2..4b7809f8261 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -36,8 +36,7 @@ build:
   before_script:
     - npm update --no-audit --no-fund --omit=dev
   script:
-    - mkdir bin/ # The minifier cannot create the location for the minified file itself
-    - npx gulp --minify --release --ci --verbosity 6
+    - npx gulp --minify --release --ci html
   artifacts:
     paths:
       - bin/FC_pregmod.html
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ea25f4e8e49..02aa37b799b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -24,7 +24,6 @@ To effectively work on the project the following tools are required:
    * Windows: Open a terminal and execute `cd C:\path\to\project\fc-pregmod`
    * GNU/Linux: Open a terminal and execute `cd /path/to/project/fc-pregmod/`
 3. Run `npm install` in your terminal
-   * On Windows, you may need to run `npm install --omit-optional` instead.
 4. Open the directory in your preferred editor
 
 * Make sure you have an extension for ESLint installed to catch formatting errors
diff --git a/build.config.json b/build.config.json
index 3d84c8e0c54..cf0aea27ddb 100644
--- a/build.config.json
+++ b/build.config.json
@@ -1,9 +1,10 @@
 {
 	"dirs": {
 		"intermediate": "build",
-		"output": "bin"
+		"output": "bin",
+		"modOutput": "bin/mods"
 	},
-	"output": "FC_pregmod.html",
+	"output": "FC_pregmod[extras].html",
 	"gitVersionFile": "src/002-config/fc-version.js.commitHash.js",
 	"sources": {
 		"module": {
@@ -11,14 +12,16 @@
 			"css": ["css/**/*.css"]
 		},
 		"story": {
-			"css": ["src/**/*.css"],
 			"js": ["src/**/*.js"],
+			"css": ["src/**/*.css"],
 			"twee": ["src/**/*.tw"],
 			"media": [
-				"src/art/vector/layers",
-				"src/art/vector_revamp/layers"
+				"src/art/vector/layers/",
+				"src/art/vector_revamp/layers/"
 			]
 		},
+		"mods": "mods/dev",
+		"themes": "themes",
 		"head": "resources/raster/favicon/arcologyVector.html"
 	},
 	"options": {
diff --git a/compile-legacy.bat b/compile-legacy.bat
new file mode 100644
index 00000000000..a07be8664a8
--- /dev/null
+++ b/compile-legacy.bat
@@ -0,0 +1,2 @@
+:: just a shortcut for compile.bat --legacy
+call compile.bat --legacy
\ No newline at end of file
diff --git a/compile-legacy.sh b/compile-legacy.sh
new file mode 100755
index 00000000000..b3b903a7461
--- /dev/null
+++ b/compile-legacy.sh
@@ -0,0 +1,203 @@
+#!/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
diff --git a/compile.bat b/compile.bat
index 869fd26680f..1ec489dd0fe 100644
--- a/compile.bat
+++ b/compile.bat
@@ -3,15 +3,103 @@
 
 :: Set working directory
 pushd %~dp0
-SETLOCAL
+SET BASEDIR=%~dp0
+SETLOCAL EnableDelayedExpansion
 
-:: If compile is invoked with "extramemory", set SORT_MEM (used by concatFiles.bat)
+set "GULP=True"
 
-SET CMD_OPTION=%~1
-IF DEFINED CMD_OPTION IF /I [%CMD_OPTION%]==[EXTRAMEMORY] SET CMD_OPTION=EXTRAMEM
-IF DEFINED CMD_OPTION IF /I [%CMD_OPTION%]==[EMEMORY] SET CMD_OPTION=EXTRAMEM
-IF DEFINED CMD_OPTION IF /I [%CMD_OPTION%]==[EXTRAMEM] SET SORT_MEM=65535
+:: 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
+	:: disable building with Gulp
+	if "%ARG%"=="--legacy" SET "GULP="
+	:: create themes using legacy compile
+	if "%ARG%"=="--themes" SET "THEMES=True"
+	if "%ARG%"=="--help" GOTO :help
+    SHIFT
+    GOTO processargs
+)
+
+:: check if gulp is defined
+if DEFINED GULP (
+	:: check for git
+	where /q git
+	IF ERRORLEVEL 1 (
+		ECHO git is not available.
+		SET "GULP="
+	)
+
+	:: check for node
+	where /q node
+	IF ERRORLEVEL 1 (
+		ECHO Node is not available.
+        SET "GULP="
+	)
+
+	:: if gulp is still defined
+	IF DEFINED GULP (
+		:: if the node_modules directory doesn't exist
+		IF NOT EXIST .\node_modules\ (
+			ECHO Node and git are available, but the Node modules are not installed.
+			CHOICE /C YN /N /M "Would you like us to run 'npm install' to install Node modules, using ~120 MB of disk space [Y/N]?"
+			IF !errorlevel!==1 (
+				ECHO Installing Node modules...
+				CALL npm install
+			) ELSE (
+				ECHO Use the '--legacy' flag to remove this prompt or run 'compile-legacy.bat' instead.
+				SET "GULP="
+			)
+		)
+	)
+
+	:: if gulp is still defined
+	if DEFINED GULP (
+		ECHO Using Gulp to build FC.
+		ECHO Pass the '--legacy' flag to use the legacy compiler or run 'compile-legacy.bat' instead.
+
+		if DEFINED THEMES (
+			ECHO The '--themes' flag only needed for legacy compiler. Gulp always compiles themes.
+			ECHO Either remove it or add the '--legacy' flag to use the legacy compiler.
+		)
+
+		:: execute gulp command
+		CALL npx gulp all --color
+
+		:: exit
+		ENDLOCAL
+		popd
+		exit /b 0
+    )
+) else (
+	:: Using legacy compiler since the '--legacy' flag was set.
+	GOTO legacybuild
+)
 
+:gulpmessage
+ECHO.
+ECHO Development builds will be more debugable (will include source maps) if you enable Gulp.
+ECHO To enable Gulp make sure you have git and Node installed.
+ECHO To remove these messages, pass the '--legacy' flag or run 'compile-legacy.bat' instead.
+ECHO.
+
+: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
@@ -52,3 +140,47 @@ IF EXIST "%~dp0src\002-config\fc-version.js.commitHash.js" DEL "%~dp0src\002-con
 ENDLOCAL
 popd
 ECHO Done
+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
+
+:: print help
+:help
+ECHO compile.bat [flags]
+ECHO.
+ECHO --legacy: build using legacy compiler (less features, but no extra depenencies)
+ECHO --themes: create themes using legacy compiler (automatically created in new compiler)
+ECHO EXTRAMEM: passed to the sort script when using legacy compiler
+ECHO --npm: install npm modules automatically
+:: exit
+GOTO :eof
\ No newline at end of file
diff --git a/compile.sh b/compile.sh
index b3b903a7461..254fb9166ac 100755
--- a/compile.sh
+++ b/compile.sh
@@ -1,203 +1,84 @@
 #!/bin/bash
 
-output=/dev/stdout
-name=FC_pregmod
+# check for git
+if command -v git &> /dev/null; then
+    git="true"
+fi
+
+# check for Node
+if command -v node &> /dev/null; then
+    node="true"
+fi
+
+# check for node_modules
+if [[ "$git" && "$node" ]]; then
+    # if the node_modules directory doesn't exist
+    if [[ -d node_modules ]]; then
+        npm="true"
+    fi
+fi
 
 # displays help text
 function displayHelp() {
 	cat <<HelpText
-Usage: compile.sh [OPTION]...
+Usage: compile.sh [GULP FLAGS]
 
-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
-}
+Falls back to ./compile-legacy.sh if Gulp's dependencies are missing.
 
-#display an error message
-function echoError() {
-	echo -e "\033[0;31m$*\033[0m"
-}
+HelpText
 
-#display message
-function echoMessage() {
-	echo "$1" >"${output}"
+if [[ $"npm" ]]; then
+    npx gulp
+else
+    cat <<HelpText
+Gulp's dependencies are not installed.
+HelpText
+fi
 }
 
-#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."
-}
+flags="all"
 
 if [[ "$1" == "" ]]; then
 	#tip if no option
-	echoMessage "For more options see compile.sh -h."
+	echo "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
-				;;
+                flags="${flags} $1"
 		esac
 		shift
 	done
 fi
 
-# Run sanity check.
-[ -n "$sanity" ] && ./sanityCheck.sh
+echo ""
+if [[ ! "$git" ]]; then
+    echo "git is not available, install it to use Gulp compiler."
+fi
 
-if ! [[ -d .git ]]; then
-	echoMessage "No git repository. Git specific actions disabled."
+if [[ ! "$node" ]]; then
+    echo "Node is not available, install it to use the Gulp compiler."
 fi
 
-#compile
-if [[ "$dry" ]]; then
-	echoMessage "Dry run finished."
-else
-	compile
-	echoMessage "Compilation finished at $(date +%T)."
+if [[ ! "$npm" ]]; then
+    echo "Node modules aren't installed, run 'npm install' to install them."
 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)"
+if [[ ! "$git" || ! "$node" || ! "$npm" ]]; then
+    echo ""
+    echo "Gulp compiler dependencies not met, falling back to legacy compiler."
+    echo "The legacy compiler is missing source map generation, but requires no other dependencies."
+    echo "If you wish to always use the legacy compiler, run './compile-legacy.sh' instead."
+    echo ""
+    ./compile-legacy.sh $flags
+    exit 0
 fi
+
+echo "Compiling using Gulp with these arguments: $flags"
+echo ""
+npx gulp $flags
\ No newline at end of file
diff --git a/compile_debug.bat b/compile_debug.bat
deleted file mode 100644
index 17d3c9be06d..00000000000
--- a/compile_debug.bat
+++ /dev/null
@@ -1,11 +0,0 @@
-@echo off
-:: Free Cities Basic Compiler - Windows
-
-:: Set working directory
-pushd %~dp0
-
-:: Compile the game
-call "%~dp0compile.bat"
-
-popd
-PAUSE
diff --git a/compile_themes.bat b/compile_themes.bat
deleted file mode 100644
index 460d1c11704..00000000000
--- a/compile_themes.bat
+++ /dev/null
@@ -1,29 +0,0 @@
-@echo off
-
-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
-EXIT /B 0
diff --git a/gulpfile.js b/gulpfile.js
index 3f34c4542ee..5141a260448 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,38 +1,119 @@
-import {config as minifyConfig, string as minifyString, file as minifyFile} from '@tdewolff/minify';
-import yargs from 'yargs';
-import {hideBin} from 'yargs/helpers';
+/**
+ * @file defines the gulp tasks that we use to build FC
+ */
+
+/* globals process */
+// cSpell: words embedsourcemaps, sourcemapsincludecontent, pmodversion, fnames, nothrow, pathcmp, autoprefix
+
+import yargs from "yargs";
+import {hideBin} from "yargs/helpers";
+import terser from "gulp-terser";
+import cleanCSS from "gulp-clean-css";
 import autoprefixer from "autoprefixer";
-import chalk from "chalk";
 import gulp from "gulp";
 import concat from "gulp-concat";
-import log from 'fancy-log-levels';
+import gulpIgnore from "gulp-ignore";
+import log from "fancy-log-levels";
 import noop from "gulp-noop";
 import postcss from "gulp-postcss";
 import shell from "gulp-shell";
 import sort from "gulp-sort";
 import sourcemaps from "gulp-sourcemaps";
 import stripCssJSComments from "gulp-strip-comments";
-import childProcess from "child_process";
+import {execSync} from "child_process";
 import which from "which";
-import fs from "fs";
+import jetpack from "fs-jetpack";
 import path from "path";
 import os from "os";
-import through from 'through2';
-import cfg from './build.config.json' assert {type: "json"};
-
-// defines build options which can be supplied from the command line
-// example: npx gulp --minify --verbosity 6
-const args = yargs(hideBin(process.argv)).options({
-	verbosity: {type: 'number', default: 1},
-	release: {type: 'boolean', default: false},
-	ci: {type: 'boolean', default: false}, // assumes gitlab CI environment
-	minify: {type: 'boolean', default: false},
-	embedsourcemaps: {type: 'boolean', default: false},
-	sourcemapsincludecontent: {type: 'boolean', default: false}
-}).argv;
-
-minifyConfig({"html-keep-comments": true, "js-keep-var-names": true});
+import {fileURLToPath} from "url";
+
+// run `npx gulp` to display help
+const args = yargs(hideBin(process.argv))
+	.showHelpOnFail(true)
+	.option('verbosity', {
+		alias: 'v',
+		type: 'number',
+		description: 'Logging verbosity level, 1-6',
+		default: 6,
+	})
+	.option('release', {
+		alias: 'r',
+		type: 'boolean',
+		description: 'Build source maps if not supplied',
+		default: false,
+	})
+	.options('ci', {
+		type: 'boolean',
+		description: 'Assumes gitlab CI environment',
+		default: false,
+	})
+	.option('minify', {
+		alias: 'm',
+		type: 'boolean',
+		description: 'Makes builds smaller at the cost of readability and compiling time',
+		default: false,
+	})
+	.options('embedsourcemaps', {
+		type: 'boolean',
+		description: 'Embed source maps into final html file',
+		default: true,
+	})
+	.options('sourcemapsincludecontent', {
+		type: 'boolean',
+		description: 'Add original js and css code to the source maps',
+		default: true,
+	})
+	.options('debug', {
+		type: 'boolean',
+		description: 'Add files that match the *.debug.* pattern, otherwise ignore them',
+		default: false,
+	})
+	.options('hash', {
+		type: 'boolean',
+		description: 'Adds the git commit hash to the output filename',
+		default: false,
+	})
+	.options('epoch', {
+		type: 'boolean',
+		description: 'Adds the current epoch time to the output filename',
+		default: false,
+	})
+	.options('pmodversion', {
+		type: 'boolean',
+		description: 'Adds the current pregmod version number to the output filename',
+		default: false,
+	})
+	// commands should exist as exported gulp tasks
+	.command('html', "Build FC")
+	.command('themes', "Build themes")
+	.command('mods', "Build mods")
+	.command('all', "Build FC, themes, and mods")
+	.demandCommand()
+	.parse();
+
+// load json without using "...assert {type: "json"}" which is still in to proposal stage
+// https://github.com/tc39/proposal-json-modules
+
+const cfg = jetpack.read(fileURLToPath(new URL('./build.config.json', import.meta.url)), "json");
 
+/**
+ * Options used to minify js code using terser
+ * @type {import("terser").MinifyOptions}
+ */
+const terserMinifyConfig = {
+	// https://www.npmjs.com/package/terser#minify-options
+	mangle: true,
+	// eslint-disable-next-line camelcase
+	keep_classnames: true,
+	// eslint-disable-next-line camelcase
+	keep_fnames: true,
+};
+/**
+ * Options used to minify css code using CleanCSS
+ */
+const cleanCssMinifyConfig = {
+	// https://www.npmjs.com/package/clean-css#constructor-options
+};
 /** Filename for the temporary output. Tweego will write here and then it will be moved into the output dir */
 const htmlOut = "tmp.html";
 
@@ -42,47 +123,56 @@ log(args.verbosity);
 // -------------- Helper functions -----------------------
 
 /**
- * @summary Locates tweego executable.
- *
- *  Looks in the host $PATH, otherwise uses one of the bundled in devTools/tweeGo
+ * Locates a tweego executable.
+ * Looks in the host $PATH, otherwise uses one of the bundled executables in devTools/tweeGo
+ * @returns {string} Returns the path to the tweego executable
  */
 function tweeCompilerExecutable() {
-	const systemTweego = which.sync('tweego', {nothrow: true});
+	const systemTweego = which.sync("tweego", {nothrow: true});
+
 	if (systemTweego) {
-		log.info('Found system tweego at ', systemTweego);
+		log.info("Found system tweego at ", systemTweego);
 		return systemTweego;
 	}
-	const archSuffix = os.arch() === 'x64' ? '64' : '86';
+
+	const archSuffix = os.arch() === "x64" ? "64" : "86";
 	const platformSuffix = {
-		'Darwin': 'osx',
-		'Linux': 'nix',
-		'Windows_NT': 'win'
+		"Darwin": "osx",
+		"Linux": "nix",
+		"Windows_NT": "win"
 	}[os.type()];
-	const extension = os.type() === 'Windows_NT' ? '.exe' : '';
-	const res = path.join('.', 'devTools', 'tweeGo', `tweego_${platformSuffix}${archSuffix}${extension}`);
-	log.info('Using bundled tweego at ', res);
+	const extension = os.type() === "Windows_NT" ? ".exe" : "";
+	const res = path.join(".", "devTools", "tweeGo", `tweego_${platformSuffix}${archSuffix}${extension}`);
+
+	log.info("Using bundled tweego at ", res);
 	return res;
 }
 
 /**
- * @summary Composes tweego invocation command
+ * Composes tweego invocation command
  *
  * Combines paths to tweego and options defined in the build.config.json file to
  * return a full tweego launch command, which will combine all story elements, pick up modules,
- * and generate an HTML file in the intermidiate directory
+ * and generate an HTML file in the intermediate directory
+ * @returns {string} Full tweego command string
  */
 function tweeCompileCommand() {
-	let sources = [path.join(cfg.dirs.intermediate, "story")];
+	const sources = [path.join(cfg.dirs.intermediate, "story")];
+
 	sources.push(...cfg.sources.story.media);
 
-	let modules = [path.join(cfg.dirs.intermediate, "module")];
-	let moduleArgs = modules.map(fn => `--module=${fn}`);
-	return `${tweeCompilerExecutable()} --head=${cfg.sources.head} -o ${path.join(cfg.dirs.intermediate, htmlOut)} ${moduleArgs.join(' ')} ${sources.join(' ')}`;
+	const modules = [path.join(cfg.dirs.intermediate, "module")];
+	const moduleArgs = modules.map(fn => `--module=${fn}`);
+
+	return `${tweeCompilerExecutable()} --head=${cfg.sources.head} -o ${path.join(cfg.dirs.intermediate, htmlOut)} ${moduleArgs.join(" ")} ${sources.join(" ")}`;
 }
 
 /**
  * gulp-sort uses String.localeCompare() by default, which may be case-insensitive,
  * while we require case -sensitive sorting for sources
+ * @param {Vinyl} a
+ * @param {Vinyl} b
+ * @returns {number}
  */
 function pathcmp(a, b) {
 	return (a.path < b.path ? -1 : (a.path > b.path ? 1 : 0));
@@ -90,9 +180,17 @@ function pathcmp(a, b) {
 
 /**
  * Creates a pipeline that sorts and combines files
+ * @param {string|string[]} srcGlob Glob(s) to combine
+ * @param {string} destDir destination directory to save to
+ * @param {string} destFileName the filename to save as
+ * @returns {NodeJS.ReadWriteStream}
  */
 function concatFiles(srcGlob, destDir, destFileName) {
 	return gulp.src(srcGlob)
+		.pipe(args.debug
+			? noop()
+			: gulpIgnore.exclude("*.debug.*")
+		)
 		.pipe(sort(pathcmp))
 		.pipe(concat(destFileName))
 		.pipe(gulp.dest(destDir));
@@ -104,26 +202,34 @@ function concatFiles(srcGlob, destDir, destFileName) {
  * The pipeline collects sources, sorts them, concatenates and
  * saves in the destination dir. If no "release" command line switch was
  * supplied, sourcemaps will be written for the files
+ * @param {string} srcGlob Glob to process
+ * @param {string} destDir destination directory to save to
+ * @param {string} destFileName the filename to save as
+ * @returns {NodeJS.ReadWriteStream}
  */
 function processScripts(srcGlob, destDir, destFileName) {
 	const addSourcemaps = !args.release;
-	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf('*')));
+	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf("*")));
+
 	return gulp.src(srcGlob)
+		.pipe(args.debug
+			? noop()
+			: gulpIgnore.exclude("*.debug.*")
+		)
 		.pipe(sort(pathcmp))
 		.pipe(addSourcemaps ? sourcemaps.init() : noop())
 		.pipe(concat(destFileName))
-		.pipe(addSourcemaps ?
-			sourcemaps.write(args.embedsourcemaps ? undefined : '.', {
+		.pipe(args.minify
+			// @ts-expect-error This is correct
+			? terser(terserMinifyConfig)
+			: noop())
+		.pipe(addSourcemaps
+			? sourcemaps.write(args.embedsourcemaps ? undefined : ".", {
 				includeContent: args.sourcemapsincludecontent,
 				sourceRoot: prefix,
 				sourceMappingURLPrefix: path.relative(cfg.dirs.output, destDir)
-			}) :
-			noop())
-		// minify JS. Mostly for the src/ directory, which later minifying won't be able to access
-		.pipe(args.minify ? through.obj((chunk, enc, cb) => {
-			chunk.contents = new Buffer(minifyString("text/javascript", chunk.contents.toString()));
-			cb(null, chunk);
-		}) : noop())
+			})
+			: noop())
 		.pipe(gulp.dest(destDir));
 }
 
@@ -133,24 +239,37 @@ function processScripts(srcGlob, destDir, destFileName) {
  * The pipeline collects sources, sorts them, concatenates, pass through
  * an autoprefixer and saves in the destination dir. If no "release" command
  * line switch was supplied, sourcemaps will be written for the files
+ * @param {string} srcGlob Glob to process
+ * @param {string} destDir destination directory to save to
+ * @param {string} destFileName the filename to save as
+ * @returns {NodeJS.ReadWriteStream}
  */
 function processStylesheets(srcGlob, destDir, destFileName) {
 	const addSourcemaps = !args.release;
-	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf('*')));
+	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf("*")));
+
 	return gulp.src(srcGlob)
+		.pipe(args.debug
+			? noop()
+			: gulpIgnore.exclude("*.debug.*")
+		)
 		.pipe(sort(pathcmp))
 		.pipe(addSourcemaps ? sourcemaps.init() : noop())
 		.pipe(concat(destFileName))
-		.pipe(cfg.options.css.autoprefix ?
-			postcss([autoprefixer({overrideBrowserslist: ['last 2 versions']})]) :
-			noop())
-		.pipe(addSourcemaps ?
-			sourcemaps.write(args.embedsourcemaps ? undefined : '.', {
+		.pipe(cfg.options.css.autoprefix
+			? postcss([autoprefixer({overrideBrowserslist: ["last 2 versions"]})])
+			: noop())
+		// minify css using CleanCSS
+		.pipe(args.minify
+			? cleanCSS(cleanCssMinifyConfig)
+			: noop())
+		.pipe(addSourcemaps
+			? sourcemaps.write(args.embedsourcemaps ? undefined : ".", {
 				includeContent: args.sourcemapsincludecontent,
 				sourceRoot: prefix,
 				sourceMappingURLPrefix: path.relative(cfg.dirs.output, destDir)
-			}) :
-			noop())
+			})
+			: noop())
 		.pipe(gulp.dest(destDir));
 }
 
@@ -161,53 +280,113 @@ function processStylesheets(srcGlob, destDir, destFileName) {
  * for writing sourcemaps we have to process them one by one, because we detect
  * where to write source maps from each glob pattern in order to make them accessible
  * from the compiled HTML
+ * @param {("module-js"|"module-css"|"story-js"|"story-css"|"story-twee"|"story-media")} name
+ * @param {Function} processorFunc
+ * @param {string[]} globs
+ * @param {string} destDir destination directory to save to
+ * @param {string} destFileName filename to save as
+ * @param  {...any} args extra args to pass to processorFunc
+ * @returns {import("gulp").TaskFunction}
  */
 function processSrc(name, processorFunc, globs, destDir, destFileName, ...args) {
-	let tasks = [];
+	/** @type {import("gulp").TaskFunction[]} */
+	const tasks = [];
+
 	if (!Array.isArray(globs) || globs.length === 1) {
 		const src = Array.isArray(globs) ? globs[0] : globs;
+
 		tasks.push(() => processorFunc(src, destDir, destFileName, args));
 		tasks[tasks.length - 1].displayName = "process-" + name;
 	} else { // many globs
 		const ext = path.extname(destFileName);
 		const bn = path.basename(destFileName, ext);
+
 		for (let i = 0; i < globs.length; ++i) {
 			tasks.push(() => processorFunc(globs[i], destDir, `${bn}-${i}${ext}`, args));
 			tasks[tasks.length - 1].displayName = `process-${name}-${i}`;
 		}
 	}
+
 	const res = gulp.parallel(...tasks);
+
 	res.displayName = name;
 	return res;
 }
 
-/** Returns true if the working directory is a Git repository */
+function gitExecutableExists() {
+	return which.sync('git', {nothrow: true}) !== null;
+}
+
+/**
+ * Returns true if the working directory is a Git repository
+ * @returns {boolean}
+ */
 function isGitCheckout() {
-	return fs.statSync('.git').isDirectory();
+	return jetpack.exists(".git") === "dir";
 }
 
-/** Invokes git and writes hash of the head commit to the file, specified in the 'gitVersionFile' build config property */
+let gitHash = "UNKNOWN";
+
+/**
+ * Invokes git and writes the hash of the head commit to the file, specified in the 'gitVersionFile' config property
+ * @param {Function} cb callback function
+ */
 function injectGitCommit(cb) {
 	// check if we are in CI mode, if yes, just read out the hash from environment variables
-	const readGitHash = args.ci ? "echo $CI_COMMIT_SHORT_SHA" : 'git rev-parse --short HEAD';
-	childProcess.exec(readGitHash, function(error, stdout) {
-		if (!error) {
-			log.info('current git hash: ', stdout);
-			fs.writeFile(cfg.gitVersionFile, `App.Version.commitHash = '${stdout.trimEnd()}';\n`, cb);
-		} else {
-			log.error(chalk.red(`Error running git. Error: ${error}`));
-			cb();
-		}
-	});
+	gitHash = args.ci
+		? execSync("echo $CI_COMMIT_SHORT_SHA").toString().trim()
+		: execSync("git rev-parse --short HEAD").toString().trim();
+
+	if (gitHash === "$CI_COMMIT_SHORT_SHA" || (gitHash === "" && args.ci === true)) {
+		// This should only fire if the CI flag is used out of GitLab CI
+		// If it fires in GitLab CI then $CI_COMMIT_SHORT_SHA is no longer valid or something has gone terribly wrong.
+		log.warn(`git hash === "${gitHash}"!`);
+		log.warn("Are you in a GitLab CI environment?");
+		log.warn("If not please remove the --ci flag from the commands arguments to remove this message.");
+		log.warn("Sleeping for 10 seconds and then setting ci to false");
+		// sleep for 10 second
+		Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10000);
+		args.ci = false;
+		return injectGitCommit(cb);
+	} else if (gitHash === "") {
+		throw new Error("Failed to get git hash!");
+	}
+
+	log.info("current git hash:", gitHash);
+	jetpack.write(cfg.gitVersionFile, `App.Version.commitHash = '${gitHash}';\n`, {atomic: true});
+	cb();
 }
 
-/** Ensures the file with the git commit hash does not exists */
+/**
+ * Ensures the file with the git commit hash does not exists
+ * @param {Function} cb callback function
+ */
 function cleanupGit(cb) {
-	if (fs.existsSync(cfg.gitVersionFile)) {
-		fs.unlink(cfg.gitVersionFile, cb);
-	} else {
-		cb();
+	jetpack.remove(cfg.gitVersionFile);
+	cb();
+}
+
+/**
+ * Copies a file/directory to a destination
+ * @param {string} source
+ * @param {string} destination
+ * @param {boolean} removeExisting
+ * @param {Function} callback
+ */
+function copy(source, destination, removeExisting, callback) {
+	// if not debug mode filter out *.debug.* files
+	let matching = "!*.debug.*";
+
+	if (args.debug === true) {
+		matching = "*";
+	}
+
+	if (removeExisting) {
+		jetpack.remove(destination);
 	}
+
+	jetpack.copy(source, destination, {overwrite: true, matching: matching});
+	callback();
 }
 
 // --------------- build tasks definitions -----------------
@@ -222,14 +401,15 @@ When all intermediate files are ready, tweego picks them up and assembles the HT
 */
 
 // Create task to execute tweego
-gulp.task('compileStory', shell.task(tweeCompileCommand(), {
+gulp.task("compileStory", shell.task(tweeCompileCommand(), {
 	env: {...process.env, ...cfg.options.twee.environment},
 	verbose: args.verbosity >= 3
 }));
 
 /**
- * Creates tasks for preparing intermidiate files for a component
- * @param {*} name "story" or "module"
+ * Creates tasks for preparing intermediate files for a component
+ * @param {"story"|"module"} name "story" or "module"
+ * @returns {import("gulp").TaskFunction}
  */
 function prepareComponent(name) {
 	const processors = {
@@ -249,17 +429,24 @@ function prepareComponent(name) {
 			func: null
 		}
 	};
-
 	const c = cfg.sources[name];
 	const outDir = path.join(cfg.dirs.intermediate, name);
 	const subTasks = [];
-	for (const srcType in c) {
+
+	/** @type {"css"|"js"|"twee"|"media"} */
+	let srcType;
+
+	for (srcType in c) {
 		const proc = processors[srcType];
+
 		if (proc.func) {
+			// @ts-expect-error ${name}-${srcType} is valid
 			subTasks.push(processSrc(`${name}-${srcType}`, proc.func, c[srcType], outDir, `${name}${proc.output}`, cfg.options[srcType]));
 		}
 	}
-	let r = gulp.parallel(subTasks);
+
+	const r = gulp.parallel(subTasks);
+
 	r.displayName = "prepare-" + name;
 	return r;
 }
@@ -267,77 +454,156 @@ function prepareComponent(name) {
 /**
  *  Creates a task for compiling a theme
  * @param {string} themeName theme directory name
+ * @returns {string} task name
  */
 function makeThemeCompilationTask(themeName) {
+	// make sure it's a name, not a path
+	if (themeName.replace(/\/+$/, "").replace(/\\+$/, "").includes(path.sep)) {
+		// only keep the last path component
+		themeName = themeName.split(path.sep).pop();
+	}
+
 	const taskName = `make-theme-${themeName}`;
+
 	gulp.task(taskName, function() {
-		return concatFiles(`themes/${themeName}/**/*.css`, cfg.dirs.output, `${themeName}.css`);
+		return concatFiles(`${cfg.sources.themes}/${themeName}/**/*.css`, cfg.dirs.output, `${themeName}.css`);
 	});
 	return taskName;
 }
 
-/** Moves compiled HTML file from the intermidiate location to the final output */
-function moveHTMLInPlace(cb) {
-	fs.rename(path.join(cfg.dirs.intermediate, htmlOut), path.join(cfg.dirs.output, cfg.output), cb);
-}
+/**
+ *  Creates a task for compiling a mod
+ * @param {string} modName mod directory name
+ * @returns {string} task name
+ */
+function makeModCompilationTask(modName) {
+	// make sure it's a name, not a path
+	if (modName.replace(/\/+$/, "").replace(/\\+$/, "").includes(path.sep)) {
+		// only keep the last path component
+		modName = modName.split(path.sep).pop();
+	}
 
-function moveAndMinifyHTML(cb) {
-	minifyFile('text/html', path.join(cfg.dirs.intermediate, htmlOut), path.join(cfg.dirs.output, cfg.output));
-	cb();
+	const taskName = `make-mod-${modName}`;
+
+	gulp.task(taskName, function(cb) {
+		return copy(`${cfg.sources.mods}/${modName}`, `${cfg.dirs.modOutput}/${modName}`, true, cb);
+	});
+	return taskName;
 }
 
-/** Removes intermediate compilation files if any */
-function removeIntermediateFiles(cb) {
-	if (fs.existsSync(cfg.dirs.intermediate)) {
-		fs.rm(cfg.dirs.intermediate, {recursive: true}, cb);
-	} else {
+/**
+ * Moves compiled HTML file from the intermediate location to the final output
+ * @param {Function} cb callback function
+ */
+function moveHTML(cb) {
+	if (jetpack.exists(path.join(cfg.dirs.intermediate, htmlOut)) === "file") {
+		let finalPath = path.join(cfg.dirs.output, cfg.output);
+		let extraString = "";
+
+		if (args.epoch) {
+			extraString += "." + String(Math.floor(Date.now() / 1000));
+		}
+
+		if (args.pmodversion) {
+			// open ./src/002-config/fc-version.js
+			jetpack.read("./src/002-config/fc-version.js").split("\n").forEach(line => {
+				if (line.trim().includes("pmod: ")) {
+					// add everything between first and last " to extraString
+					extraString += "." + line.split("\"")[1].split("\"")[0];
+				}
+			});
+		}
+
+		if (args.hash) {
+			extraString += "." + gitHash;
+		}
+
+		finalPath = finalPath.replace("[extras]", extraString);
+
+		jetpack.move(path.join(cfg.dirs.intermediate, htmlOut), path.resolve(finalPath), {overwrite: true});
+
+		log.info("FC saved to \"" + finalPath + "\"");
 		cb();
 	}
 }
 
+/**
+ *  Removes intermediate compilation files if any
+ * @param {Function} cb callback function
+ */
+function removeIntermediateFiles(cb) {
+	jetpack.remove(cfg.dirs.intermediate);
+	cb();
+}
+
 // Creates task to assemble components in the intermediate dir where they are ready for tweego
-gulp.task('prepare', gulp.parallel(prepareComponent("module"), prepareComponent("story")));
+gulp.task("prepare", gulp.parallel(prepareComponent("module"), prepareComponent("story")));
+
+// Creates theme build task for each subdirectory in the 'themes' dir
+const themeTasks = jetpack.find(cfg.sources.themes, {files: false, directories: true, recursive: false})
+	.map(entry => makeThemeCompilationTask(entry));
+
+// create cfg.sources.mods directory if it doesn't exist
+jetpack.dir(cfg.sources.mods);
+
+// Creates mod build task for each subdirectory in the 'mods' dir
+const modTasks = jetpack.find(cfg.sources.mods, {files: false, directories: true, recursive: false})
+	.map(entry => makeModCompilationTask(entry));
+
+// If modTasks is empty, create a task that does nothing
+if (modTasks.length === 0) {
+	gulp.task("make-mod-empty-folder", function(cb) {
+		return cb();
+	});
+
+	modTasks.push("make-mod-empty-folder");
+}
 
 // Create the main build and clean tasks, which include writing Git commit hash if we are working in a Git repo
 let cleanOp = noop();
-if (isGitCheckout()) {
+
+if (isGitCheckout() && gitExecutableExists()) {
 	cleanOp = gulp.parallel(cleanupGit, removeIntermediateFiles);
-	gulp.task('buildHTML', gulp.series(cleanOp, injectGitCommit, 'prepare', 'compileStory', cleanupGit));
+
+	gulp.task("buildHTML", gulp.series(cleanOp, injectGitCommit, "prepare", "compileStory", cleanupGit));
 } else if (args.ci) {
 	// CI environment is already clean
-	gulp.task('buildHTML', gulp.series(injectGitCommit, 'prepare', 'compileStory'));
+
+	gulp.task("buildHTML", gulp.series(injectGitCommit, "prepare", "compileStory"));
 } else {
+	if (isGitCheckout()) {
+		log.info("git executable not found.");
+	}
 	cleanOp = gulp.parallel(removeIntermediateFiles);
-	gulp.task('buildHTML', gulp.series(cleanOp, 'prepare', 'compileStory'));
+	gulp.task("buildHTML", gulp.series(cleanOp, "prepare", "compileStory"));
 }
-export const clean = cleanOp;
 
-// Creates theme build task for each subdirectory in the 'themes' dir
-const themeTasks = fs.readdirSync('themes')
-	.filter(entry => fs.statSync(path.join('themes', entry)).isDirectory())
-	.map(entry => makeThemeCompilationTask(entry));
+export const clean = cleanOp;
 
 // Create user-invocable targets for building HTML and themes
-export const html = gulp.series('buildHTML', args.minify ? moveAndMinifyHTML : moveHTMLInPlace);
+export const html = gulp.series("buildHTML", moveHTML);
 export const themes = gulp.parallel(themeTasks);
+export const mods = gulp.parallel(modTasks);
 
-// Set the default target which is invoked when no target is explicitly provided by user
-export default html;
-// Convenient shortcut to build everything (HTML and themes)
-export const all = gulp.parallel(html, themes);
+// Convenient shortcut to build everything (HTML, themes, and mods)
+export const all = gulp.parallel(html, themes, mods);
 
 // legacy tasks
 
-gulp.task('twineCSS', function() {
-	return concatFiles([...cfg.sources.module.css, ...cfg.sources.story.css], 'devNotes', 'twine CSS.txt');
+gulp.task("twineCSS", function() {
+	return concatFiles([...cfg.sources.module.css, ...cfg.sources.story.css], "devNotes", "twine CSS.txt");
 });
 
-gulp.task('twineJS', function() {
-	return gulp.src([...cfg.sources.module.js, ...cfg.sources.story.js, '!src/art/assistantArt.js'])
+gulp.task("twineJS", function() {
+	return gulp.src([...cfg.sources.module.js, ...cfg.sources.story.js, "!src/art/assistantArt.js"])
+		.pipe(args.debug
+			? noop()
+			: gulpIgnore.exclude("*.debug.*")
+		)
 		.pipe(stripCssJSComments({trim: true}))
 		.pipe(sort(pathcmp))
-		.pipe(concat('twine JS.txt'))
-		.pipe(gulp.dest('devNotes'));
+		.pipe(concat("twine JS.txt"))
+		.pipe(gulp.dest("devNotes"));
 });
 
-export const twine = gulp.parallel('twineCSS', 'twineJS');
+export const twine = gulp.parallel("twineCSS", "twineJS");
diff --git a/package.json b/package.json
index db57146c9d8..549a90cb95a 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,10 @@
 	},
 	"scripts": {
 		"lint": "eslint src/**/*.js js/**/*.js",
-		"compile": "sh compile.sh"
+		"compile": "sh compile.sh",
+		"build": "npx gulp all",
+		"build:debug": "npx gulp --debug all",
+		"build:release": "npx gulp --minify --release all"
 	},
 	"license": "GPL-3.0-only",
 	"devDependencies": {
@@ -26,22 +29,21 @@
 	},
 	"dependencies": {
 		"autoprefixer": "^10.0.0",
-		"chalk": "^5.2.0",
 		"fancy-log-levels": "^1.0.0",
+		"fs-jetpack": "^5.1.0",
 		"gulp": "^4.0.2",
+		"gulp-clean-css": "^4.3.0",
 		"gulp-concat": "^2.6.1",
+		"gulp-ignore": "^3.0.0",
 		"gulp-noop": "^1.0.1",
 		"gulp-postcss": "^9.0.0",
 		"gulp-shell": "^0.8.0",
 		"gulp-sort": "^2.0.0",
 		"gulp-sourcemaps": "^3.0.0",
 		"gulp-strip-comments": "^2.5.2",
+		"gulp-terser": "^2.1.0",
 		"postcss": "^8.4.21",
-		"through2": "^4.0.2",
 		"which": "^3.0.0",
 		"yargs": "^17.7.1"
-	},
-	"optionalDependencies": {
-		"@tdewolff/minify": "^2.20.9"
 	}
 }
diff --git a/compile_debug+sanityCheck.bat b/sanityCheck.bat
similarity index 84%
rename from compile_debug+sanityCheck.bat
rename to sanityCheck.bat
index 6091542f890..66eb8252085 100644
--- a/compile_debug+sanityCheck.bat
+++ b/sanityCheck.bat
@@ -1,5 +1,4 @@
 @echo off
-:: Free Cities Basic Compiler - Windows
 
 :: Set working directory
 pushd %~dp0
@@ -20,11 +19,8 @@ for %%k in (HKCU HKLM) do (
 :FOUND
 if %GITFOUND% == yes (
 	set "PATH=%GIT%bin;%PATH%"
+	echo Running sanityCheck.sh using git bash
 	bash --login -c "./sanityCheck.sh"
+) else (
+	echo Could not find git installation
 )
-
-:: Compile the game
-call "%~dp0compile.bat"
-
-popd
-PAUSE
-- 
GitLab