diff --git a/.eslintrc.json b/.eslintrc.json index 9dbbf5be39e4ecde617e97743837e526704bbaac..0306fb66558d91953ba1e547dee33e7c5536673e 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 9ca9221b11eb6d8b3aa5092a332181bd8e1dc33e..12190558f14c6997086dd9ccd25f5a092f477d92 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 621997da4f2b4f92e3b8366a1f65199516778aba..c739293998e23dbf94747126970b7a9a51ad6e4a 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 html --minify --release --ci artifacts: paths: - bin/FC_pregmod.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea25f4e8e4962f5ba82153b7712c34fce86e2603..02aa37b799b3b34b9bd841ba8f6b7764c252624d 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 3d84c8e0c546420444b7acef1a4350f826b14542..cf0aea27ddb15ce0b1de180d3faf668e1b757d02 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 0000000000000000000000000000000000000000..448dc9c4eb402913250aa1fac9a46f7c4838fa2a --- /dev/null +++ b/compile-legacy.bat @@ -0,0 +1,125 @@ +@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 + 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 +<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 +:: exit +ENDLOCAL +popd +exit /b 0 \ No newline at end of file diff --git a/compile-legacy.sh b/compile-legacy.sh new file mode 100755 index 0000000000000000000000000000000000000000..b3b903a7461b3bb9545bccc3ba8a303799260ab2 --- /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 869fd26680f7949056d3e81da223340c601d30e8..5dbdc2df58bb4d0dde14f31c0f206b47e65dd38b 100644 --- a/compile.bat +++ b/compile.bat @@ -1,54 +1,83 @@ @echo off -:: Free Cities Basic Compiler - Windows +:: Free Cities Compiler - Windows :: 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 ( + :: disable building with Gulp + if "%ARG%"=="--legacy" SET "GULP=" + 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=" + ) -:: 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 + :: 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=" ) ) ) -) -: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" + :: 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. + + :: execute gulp command + CALL npx gulp all --color + + :: keep window open instead of closing it + <nul set /p "=Press any key to exit" + pause >nul + + :: exit + ENDLOCAL + popd + exit /b 0 + ) ) -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. +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. + +:: Using legacy compiler since the '--legacy' flag was set or depenencies were missing. +CALL compile-legacy.bat +:: exit ENDLOCAL popd -ECHO Done +exit /b 0 \ No newline at end of file diff --git a/compile.sh b/compile.sh index b3b903a7461b3bb9545bccc3ba8a303799260ab2..254fb9166ace184d563d2abe3fd3b93df00fc43c 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 17d3c9be06dded99c9e1494c63945fb3f7ef33e1..0000000000000000000000000000000000000000 --- 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 460d1c11704b80a49674276031f87aa63a2f66c0..0000000000000000000000000000000000000000 --- 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 3f34c4542ee997562f5b78dcf8199e26a841a1bb..f929437c96d7198fd4f8fd848f9f398be5371ef0 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. Loses file names and paths!', + default: false, + }) + .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/js/003-data/policiesData.js b/js/003-data/policiesData.js index 5d9b5580b9f0aa07b023b5955f3dbf41910ea0fe..5185e83770dc8119ee38bd76d9464adda5fb42e2 100644 --- a/js/003-data/policiesData.js +++ b/js/003-data/policiesData.js @@ -208,7 +208,7 @@ App.Data.Policies.Selection = { { title: "Quality Height Standards (Short)", text: "no slaves of above average height for their age may be sold in the slave markets.", - activatedText: "no slaves of average average height for their age may be sold in the slave markets.", + activatedText: "no slaves of average height for their age may be sold in the slave markets.", get requirements() { return ( V.policies.SMR.height.basicSMR === 0 && diff --git a/src/gui/svgFilters.tw b/js/003-data/svgFilters.js similarity index 98% rename from src/gui/svgFilters.tw rename to js/003-data/svgFilters.js index 14dd0a0cdb527e8a2a7cedbdf6967cf7238dc1f3..21834174257dd5a15b6ccde4601b99e1a4ecc426 100644 --- a/src/gui/svgFilters.tw +++ b/js/003-data/svgFilters.js @@ -1,6 +1,4 @@ -:: SVG filters - -<svg class="defs-only" style="width: 0; height: 0; position: absolute;"> +App.Data.Art.legacySVGFilters = `<svg class="defs-only" style="width: 0; height: 0; position: absolute;"> <filter id="skin-black" color-interpolation-filters="sRGB"> <feColorMatrix type="matrix" values="0.15 0 0 0 0 0 0.1 0 0 0 0 0 0.05 0 0 0 0 0 1 0"/> </filter> @@ -211,4 +209,4 @@ <filter id="hair-white-blonde" color-interpolation-filters="sRGB"> <feColorMatrix type="matrix" values="0.7 0 0 0 0.5 0 0.7 0 0 0.5 0 0 0.5 0 0.4 0 0 0 1 0"/> </filter> -</svg> +</svg>`; diff --git a/package.json b/package.json index db57146c9d86039392d991606347e7143e86ca9f..5dd7f88b2b9c5cc6b1ba54539486c20bb80d2a78 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ }, "scripts": { "lint": "eslint src/**/*.js js/**/*.js", - "compile": "sh compile.sh" + "compile": "sh compile.sh", + "build": "npx gulp all", + "build:debug": "npx gulp all --debug", + "build:release": "npx gulp all --minify --release", + "serve": "npx http-server --port 6969 -c-1" }, "license": "GPL-3.0-only", "devDependencies": { @@ -21,27 +25,27 @@ "eslint": "^8.0.0", "eslint-plugin-jsdoc": "^46.0.0", "eslint-plugin-sonarjs": "^0.23.0", + "http-server": "^14.1.1", "ts-essentials": "^9.1.1", "typescript": "^4.4.0" }, "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 73% rename from compile_debug+sanityCheck.bat rename to sanityCheck.bat index 6091542f890e9d59ef9ba4932c14d7fd3cb985ee..e867346942d7ababe5c41edfde09e5d6d5a9cb98 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,13 @@ 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 +:: keep window open instead of closing it +<nul set /p "=Press any key to exit" +pause >nul +popd \ No newline at end of file diff --git a/serve.bat b/serve.bat new file mode 100644 index 0000000000000000000000000000000000000000..efc80fb5d864cf0aa07af348ab608742dd4f10b1 --- /dev/null +++ b/serve.bat @@ -0,0 +1,28 @@ +@echo off +:: run a Node based web server + +:: Set working directory +pushd %~dp0 +SET BASEDIR=%~dp0 + +:: check for node +where /q node +IF ERRORLEVEL 1 ( + ECHO Node is required to run this http server. + exit /b 0 +) + +:: if the node_modules directory doesn't exist +IF NOT EXIST .\node_modules\ ( + ECHO Node available, but the required 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 Requirements not met, exiting... + exit /b 0 + ) +) + +CALL npx http-server --port 6969 -c-1 \ No newline at end of file diff --git a/src/art/genAI/piercingsPromptPart.js b/src/art/genAI/piercingsPromptPart.js index a677580933a6b0df2b1072cda496ca6baaeba936..3e49b5924ef4c43510f4ee67733eb5b9789ebb55 100644 --- a/src/art/genAI/piercingsPromptPart.js +++ b/src/art/genAI/piercingsPromptPart.js @@ -13,7 +13,7 @@ App.Art.GenAI.PiercingsPromptPart = class PiercingsPromptPart extends App.Art.Ge if (this.slave.piercing.ear.weight > 0) { if (this.slave.fuckdoll === 0 || this.slave.race === "catgirl") { // covered by fuckdoll mask or fur let desc = this.slave.piercing.ear.desc ? (pronounsForSlaveProp(this.slave, this.slave.piercing.ear.desc) + ` `) : ``; - piercingParts.push(`${desc}ear piercing`); + piercingParts.push(`${desc}earrings`); } } if (this.slave.piercing.eyebrow.weight > 0) { diff --git a/src/art/vector/VectorArtJS.js b/src/art/vector/VectorArtJS.js index e34bee4be6c3d2843e0afb4e2c5c6f913cb70070..6173d19ffb82cb89147512fb27711671dc9611f7 100644 --- a/src/art/vector/VectorArtJS.js +++ b/src/art/vector/VectorArtJS.js @@ -3070,7 +3070,7 @@ App.Art.legacyVectorArtElement = function() { let needBoobs = true; const res = document.createDocumentFragment(); - res.appendChild(App.Utils.htmlToElement(App.Utils.passageElement("SVG filters").textContent)); + res.appendChild(App.Utils.htmlToElement(App.Data.Art.legacySVGFilters)); /* Set skin color */ let skinFilter = `filter: url(#skin-${_.kebabCase(slave.skin)});`; diff --git a/src/art/vector/layers/Art_Vector_Bulge_Outfit_Hard_5.svg b/src/art/vector/layers/Art_Vector_Bulge_Outfit_Hard_5.svg index 2aaa59c6c8e39b9c942c52124510f54939ef846e..9e37bfd75dc6120ca549123a44876ccb5feea94a 100644 --- a/src/art/vector/layers/Art_Vector_Bulge_Outfit_Hard_5.svg +++ b/src/art/vector/layers/Art_Vector_Bulge_Outfit_Hard_5.svg @@ -1 +1 @@ -<svg viewBox="0 0 560 1000"><path style="display:inline;fill:#000000;opacity:1;stroke-width:1.59767699" d="m 281.7204,486.15189 c -8.27152,5.36465 -19.2776,5.48718 -24.5204,2.12706 -10.32742,-6.61884 -9.52338,-22.53478 -6.95922,-26.9241 1.04021,-1.78061 3.31173,-3.81253 7.31746,-15.45565 2.67091,-7.76331 4.45802,-29.89392 4.35246,-47.23448 -0.0157,-2.57692 -2.0689,-0.0607 -2.54642,-2.58003 -0.67693,-3.57136 3.67581,-9.64775 5.05495,-11.46552 5.05654,-6.66479 7.49152,-2.94594 8.6191,-2.86266 2.96964,0.21932 3.94874,7.0831 4.55344,15.43818 0.15975,2.20754 -0.17237,2.94666 -0.29284,5.24148 -0.70014,13.33751 -2.74138,21.83974 -1.64408,43.98317 0.34564,6.97468 3.79787,15.46416 5.18284,17.82675 5.35992,9.14346 5.71676,17.33308 0.88271,21.9058 z" id="path3063"/><path id="path3065" d="m 259.14563,488.06058 c -10.96914,-4.88538 -11.06734,-23.02871 -6.9506,-27.78137 1.72999,-1.99724 2.92687,-3.77072 6.67478,-14.46803 2.57236,-7.34203 4.15037,-31.95035 4.06077,-48.57322 -0.0134,-2.47028 -3.01776,0.49194 -2.84525,-1.95083 0.24254,-3.43418 2.37195,-7.04149 6.51236,-12.58362 2.4702,-3.30651 7.55371,-3.42036 11.22685,0.0996 2.08181,1.99496 4.92216,6.02077 5.4354,14.03005 0.13561,2.11617 -2.45572,-0.0943 -2.55798,2.10562 -0.59426,12.78549 -1.81197,26.41735 -0.8806,47.6443 0.29337,6.68601 2.37262,11.24431 3.71451,13.41468 5.41921,8.76503 7.46195,22.4666 0.47332,26.86943 -6.98864,4.40281 -19.84538,3.42838 -24.86356,1.19341 z" class="skin scrotum"/> +<svg viewBox="0 0 560 1000"><path style="display:inline;fill:#000000;opacity:1;stroke-width:1.59767699" d="m 281.7204,486.15189 c -8.27152,5.36465 -19.2776,5.48718 -24.5204,2.12706 -10.32742,-6.61884 -9.52338,-22.53478 -6.95922,-26.9241 1.04021,-1.78061 3.31173,-3.81253 7.31746,-15.45565 2.67091,-7.76331 4.45802,-29.89392 4.35246,-47.23448 -0.0157,-2.57692 -2.0689,-0.0607 -2.54642,-2.58003 -0.67693,-3.57136 3.67581,-9.64775 5.05495,-11.46552 5.05654,-6.66479 7.49152,-2.94594 8.6191,-2.86266 2.96964,0.21932 3.94874,7.0831 4.55344,15.43818 0.15975,2.20754 -0.17237,2.94666 -0.29284,5.24148 -0.70014,13.33751 -2.74138,21.83974 -1.64408,43.98317 0.34564,6.97468 3.79787,15.46416 5.18284,17.82675 5.35992,9.14346 5.71676,17.33308 0.88271,21.9058 z" id="path3063"/><path id="path3065" d="m 259.14563,488.06058 c -10.96914,-4.88538 -11.06734,-23.02871 -6.9506,-27.78137 1.72999,-1.99724 2.92687,-3.77072 6.67478,-14.46803 2.57236,-7.34203 4.15037,-31.95035 4.06077,-48.57322 -0.0134,-2.47028 -3.01776,0.49194 -2.84525,-1.95083 0.24254,-3.43418 2.37195,-7.04149 6.51236,-12.58362 2.4702,-3.30651 7.55371,-3.42036 11.22685,0.0996 2.08181,1.99496 4.92216,6.02077 5.4354,14.03005 0.13561,2.11617 -2.45572,-0.0943 -2.55798,2.10562 -0.59426,12.78549 -1.81197,26.41735 -0.8806,47.6443 0.29337,6.68601 2.37262,11.24431 3.71451,13.41468 5.41921,8.76503 7.46195,22.4666 0.47332,26.86943 -6.98864,4.40281 -19.84538,3.42838 -24.86356,1.19341 z" class="skin scrotum"/></svg> diff --git a/src/cheats/mod_EditChildCheatNew.tw b/src/cheats/mod_EditChildCheatNew.tw index de30cf1d3eff93f2fcbec6ae2709f7cffef3cc99..bf2eb9273752688e0a5d1600f4ef8e7f573fafc2 100644 --- a/src/cheats/mod_EditChildCheatNew.tw +++ b/src/cheats/mod_EditChildCheatNew.tw @@ -196,7 +196,7 @@ <<link "Nationality:">> <<if (ndef _natR) || (_natR == 0) >> <<replace "#nation">> - <<set _natR =1>> + <<set _natR = 1>> <br>Current Nationality : @@.yellow;$tempSlave.nationality@@ <br> <br>Non-Nations<br> <<NOptions "Slave" >> @@ -1802,7 +1802,7 @@ <br><br> ''Puberty (pre: 0 | post: 1):'' - <<if ($tempSlave.pubertyXX == 1)||(ndef $tempSlave.pubertyXX)>> + <<if ($tempSlave.pubertyXX == 1) || (ndef $tempSlave.pubertyXX)>> @@.yellow;Post puberty@@ <<checkbox "$tempSlave.pubertyXX" 0 1 checked>> <<else>> @@ -2047,7 +2047,7 @@ <br> ''Puberty (pre: 0 | post: 1):'' - <<if ($tempSlave.pubertyXY == 1)||(ndef $tempSlave.pubertyXY)>> + <<if ($tempSlave.pubertyXY == 1) || (ndef $tempSlave.pubertyXY)>> @@.yellow;Post puberty@@ <<checkbox "$tempSlave.pubertyXY" 0 1 checked>> <<else>> diff --git a/src/cheats/mod_EditInfantCheatNew.tw b/src/cheats/mod_EditInfantCheatNew.tw index 8b6a514b04f723773e2bcdf8ad2795df981e02f9..25e7d97bf3b8d6348ab2ac1e4fb0b9ea48aec121 100644 --- a/src/cheats/mod_EditInfantCheatNew.tw +++ b/src/cheats/mod_EditInfantCheatNew.tw @@ -198,7 +198,7 @@ <<link "Nationality:">> <<if (ndef _natR) || (_natR == 0) >> <<replace "#nation">> - <<set _natR =1>> + <<set _natR = 1>> <br>Current Nationality : @@.yellow;$tempSlave.nationality@@ <br> <br>Non-Nations<br> <<NOptions "Slave" >> @@ -1804,7 +1804,7 @@ <br><br> ''Puberty (pre: 0 | post: 1):'' - <<if ($tempSlave.pubertyXX == 1)||(ndef $tempSlave.pubertyXX)>> + <<if ($tempSlave.pubertyXX == 1) || (ndef $tempSlave.pubertyXX)>> @@.yellow;Post puberty@@ <<checkbox "$tempSlave.pubertyXX" 0 1 checked>> <<else>> @@ -2049,7 +2049,7 @@ <br> ''Puberty (pre: 0 | post: 1):'' - <<if ($tempSlave.pubertyXY == 1)||(ndef $tempSlave.pubertyXY)>> + <<if ($tempSlave.pubertyXY == 1) || (ndef $tempSlave.pubertyXY)>> @@.yellow;Post puberty@@ <<checkbox "$tempSlave.pubertyXY" 0 1 checked>> <<else>> diff --git a/src/endWeek/reports/personalAttention.js b/src/endWeek/reports/personalAttention.js index f1a15f4ec3cb164ef686ee7eab6096e397b1aa74..90f3963230e7d02724dcca0293c4e258581eb805 100644 --- a/src/endWeek/reports/personalAttention.js +++ b/src/endWeek/reports/personalAttention.js @@ -727,7 +727,7 @@ App.PersonalAttention.slaveReport = function(slave) { r.push(`girls, so clearly the best way to overcome this is to teach ${him} to love the touch of one; it <span class="stat drop">backfires spectacularly.</span>`); slave.training = 0; } else { - r.push(`girls, so clearly the best way to overcome this is to give ${him} a thorough dicking; it works <span class="stat gain">better than expected</span>.`); + r.push(`girls, so clearly the best way to overcome this is to give ${him} a thorough dicking; it works <span class="stat gain">better than expected.</span>`); slave.training += 5; seX(slave, "oral", V.PC, "penetrative", 7); if (slave.vagina > 0) { @@ -2398,7 +2398,7 @@ App.PersonalAttention.slaveReport = function(slave) { r.push(App.UI.DOM.makeElement("span", `draws closer`, ["devotion", "inc"])); r.push(`to anyone able to keep up with ${his} sex drive; ${he} understands that ${he} can`); r.push(App.UI.DOM.makeElement("span", `trust in you`, ["trust", "inc"])); - r.push(`to satisfy ${his} needs, even it it's only for your own sake.`); + r.push(`to satisfy ${his} needs, even if it's only for your own sake.`); slave.energy = Math.clamp(slave.energy + 1, 0, 100); slave.devotion += 6; slave.trust += 6; diff --git a/src/events/REM/remFluctuations.js b/src/events/REM/remFluctuations.js index 540ed395293f0235d7eea60403ed1ed298eaa1a8..050e991176b5c468a1c856b3ed36bd6f0d870ae9 100644 --- a/src/events/REM/remFluctuations.js +++ b/src/events/REM/remFluctuations.js @@ -147,7 +147,7 @@ App.Events.REMFluctuations = class REMFluctuations extends App.Events.BaseEvent r.push(`He's looking unusually businesslike, reading a list titled "Hell's Holes".`); break; case "witch": - r.push(`He's looking unusually businesslike, nose first in an a book title "Economics and You".`); + r.push(`He's looking unusually businesslike, nose first in a book titled "Economics and You".`); break; case "ERROR_1606_APPEARANCE_FILE_CORRUPT": r.push(`He's looking unusually businesslike, wearing an ill-fitted business suit. ${HisA} blouse buttons pop off as ${hisA} belly swells grotesquely, before the object within ${himA} begins steadily moving upwards.`); diff --git a/src/events/RESS/review/birthdaySex.js b/src/events/RESS/review/birthdaySex.js index ba5eed43a0fa5f6a9258f32e3fee647efd9c8f96..89d3bf5732fe0f68195d12c56dd7ca473f1a6283 100644 --- a/src/events/RESS/review/birthdaySex.js +++ b/src/events/RESS/review/birthdaySex.js @@ -193,7 +193,7 @@ App.Events.RESSBirthdaySex = class RESSBirthdaySex extends App.Events.BaseEvent } else if (eventSlave.dick > 5 || eventSlave.clit > 4) { r.push(`Its girth is killing you with pain, and you know ${he} hasn't even pushed it halfway in. ${playerVirgin ? `Being <span class="virginity loss">(no longer) a${playerAVirgin ? "n anal" : ""} virgin</span> doesn't help at all${playerVVirgin && V.PC.vaginaLube === 0 || playerAVirgin ? `, nor does the lack of natural lubrication` : ""}. ` : ""}But you have made a promise to ${him} and you are not going to go back on it. ${His} birthday gift won't end until ${he} has ${his} orgasm while inside you.`); } else if (playerVirgin) { - r.push(`${His} slave's ${intruder} <span class="virginity loss">takes your${playerAVirgin ? " anal" : ""} virginity</span>.`); + r.push(`${His} slave's ${intruder} <span class="virginity loss">takes your${playerAVirgin ? " anal" : ""} virginity.</span>`); } r.push(`${He} withdraws a little and enters you again, this time all the way. You are ready to start getting pumped with a good fuck, but the slave remains still, motionless, inside you. You feel ${his} labored breathing quicken. The excitement of ${playerVirgin ? "deflowering" : "sodomizing"} ${his} own ${Master} has been too much for ${him} and ${he} is cumming on the first thrust. You can feel ${his} ${penetrationTool(eventSlave)} throbbing inside you while ${eventSlave.clit > 2 ? `the fluids from ${his} pussy soak your skin` : `${he} empties ${his} load as far as ${he} can inside you`}.`); r.push(`Once ${he}'s finished with ${his} birthday present, ${he} pulls out, leaving you empty${playerVirgin || eventSlave.clit > 3 || eventSlave.dick > 5 ? ", sore" : ""} and horny. Disappointed, but trying to be gentle, you take ${his} head and direct it to your crotch. ${He} understands ${he} must ${V.PC.vagina >= 0 ? `eat your pussy`: "suck your cock"}, and ${he} does. ${He} has come, but you haven't, and this cannot be tolerated.`); diff --git a/src/events/RESS/review/desperatelyHorny.js b/src/events/RESS/review/desperatelyHorny.js index 0910104a3628e3f5999d539706c561f3b7ebb642..e1c001f49d7d44062009b7d318830696ec9ec874 100644 --- a/src/events/RESS/review/desperatelyHorny.js +++ b/src/events/RESS/review/desperatelyHorny.js @@ -430,7 +430,7 @@ App.Events.RESSDesperatelyHorny = class RESSDesperatelyHorny extends App.Events. } r.push(`${He} wraps ${his} legs around the back of the chair and hugs your knees with ${his} arms, securing ${himself}`); if (eventSlave.belly >= 100000) { - r.push(`to you as an a cockbun for as long as you feel like keeping`); + r.push(`to you as a cockbun for as long as you feel like keeping`); if (PC.dick !== 0) { r.push(`your penis wrapped in a happy buttslut.`); } else { diff --git a/src/events/RESS/review/shiftDoorframe.js b/src/events/RESS/review/shiftDoorframe.js index d0cfc0ee025c705a9f6cb1e01ae13be988ef2350..55aeef10a8fa62e731197fec7ab085319b6172ee 100644 --- a/src/events/RESS/review/shiftDoorframe.js +++ b/src/events/RESS/review/shiftDoorframe.js @@ -813,7 +813,7 @@ App.Events.RESSShiftDoorframe = class RESSShiftDoorframe extends App.Events.Base } else { r.push(`You sigh as you feel ${him} slip ${his} cute dick into your tight`); if (V.PC.anus === 0) { - r.push(`rear, <span class="virginity loss">taking your anal virginity</span>;`); + r.push(`rear, <span class="virginity loss">taking your anal virginity;</span>`); V.PC.anus++; } else { r.push("rear;"); diff --git a/src/events/scheduled/sePCBirthday.desc.js b/src/events/scheduled/sePCBirthday.desc.js index a609c995ac881fae83656c3baf03d36616bbae31..f52c4f1696b76003c1575c71f347f490839c589b 100644 --- a/src/events/scheduled/sePCBirthday.desc.js +++ b/src/events/scheduled/sePCBirthday.desc.js @@ -730,7 +730,7 @@ App.Events.pcBirthday.Desc = (function(bday) { new App.Events.Result("Something fun and spunky; when you said party, you meant it", () => { data.attire = "casual"; return ` - <p>You find something that that broadcasts exactly what you want from this party: a raunchy, good time.</p> + <p>You find something that broadcasts exactly what you want from this party: a raunchy, good time.</p> ` + this.renderPartyScene_Arrival(data) + afterParty; }), new App.Events.Result(`The birthday ${getPronouns(V.PC).boy} will wear a birthday suit`, () => { diff --git a/src/facilities/brothel/brothelAssignmentScene.js b/src/facilities/brothel/brothelAssignmentScene.js index 0855304e682657578140cde35bd5e9cbf4f1647c..66f59f2fa4a75cddc3b4627695002ebd815b486c 100644 --- a/src/facilities/brothel/brothelAssignmentScene.js +++ b/src/facilities/brothel/brothelAssignmentScene.js @@ -316,7 +316,7 @@ App.Facilities.Brothel.assignmentScene = function(slave) { Spoken(slave, `"Yes, ${Master},"`), `${he} says obediently. ${He} hesitates, looking concerned.` ); - switch (slave.sexualFlaw) { + switch (slave.sexualFlaw) { // FIXME: needs text for paraphilias, or they need a different branch; probably the same thing is needed in other branches too case "hates oral": r.push( Spoken(slave, `"I — I'm going to h-have to suck a lot of dick there, aren't I."`), diff --git a/src/facilities/pit/pit.js b/src/facilities/pit/pit.js index 7129b4f79e7a520d65a62d18390b4460bf173e93..3f0b4581423e89279390bde815c893c29ff466a0 100644 --- a/src/facilities/pit/pit.js +++ b/src/facilities/pit/pit.js @@ -155,7 +155,7 @@ App.Facilities.Pit.pit = function() { "Roman Revivalist": `is a circular Roman amphitheater-like structure with a coffered dome built of limestone. Walls are covered with mosaics depicting various idealized gladiatorial fights. At the bottom, the pit is covered with a fine layered of sand.`, "Neo-Imperialist": `is a futurist, gothic-styled indoor list field with a retractable ceiling and a modular arena where tournaments are held. While most of them are used for slaves fighting, ${V.arcologies[0].name}'s citizens may enjoy and participate in neo-jousting with highly-powered motorcycles and modern mock battles with heavily-armored knights.`, "Aztec Revivalist": `is a large rectangular masonry structure used for both Mesoamerican ballgames and slave fights decorated in the traditional Aztec way, with stacked stone walls painted with bright murals.`, - "Egyptian Revivalist": `is a a simple sunken pit with a sand floor and sandstone walls. In the seating area, there are papyriform columns supporting the ceiling while the walls are decorated with hieroglyphic and pictorial frescoes.`, + "Egyptian Revivalist": `is a simple sunken pit with a sand floor and sandstone walls. In the seating area, there are papyriform columns supporting the ceiling while the walls are decorated with hieroglyphic and pictorial frescoes.`, "Edo Revivalist": `is a lush Japanese garden surrounding a pond filled with large, colorful koi. A red wooden footbridge links the garden with the small island that lies in the middle of the pond, which is where the slaves fight.`, "Arabian Revivalist": `is a riad, a symmetrical indoor garden centered around the fighting area. Seating for guests are available under the shade of the flora and the surrounding balconies decorated with complex arabesque.`, "Chinese Revivalist": `is decorated like a traditional Chinese courtyard, with a large open area in the center surrounded by low buildings with brick walls and clay tile roofs. A couple of bronze-cast Chinese guardian lions protect the entrance of the structure.`, diff --git a/src/interaction/main/walkPast.js b/src/interaction/main/walkPast.js index b8af584a67d349051c173b461db88323af8829d8..19d1c40d247fd1bd8f56d94dca3be3ce8cb53f98 100644 --- a/src/interaction/main/walkPast.js +++ b/src/interaction/main/walkPast.js @@ -592,7 +592,7 @@ globalThis.walkPast = (function() { } else if (fuckSeed > 90 && hasBothLegs(activeSlave)) { t += `${name} has ${partnerName} on ${his} knees and is forcibly fucking ${his2} pussy doggy style while {he2} struggles to get away.`; } else if (fuckSeed > 80 && hasAnyArms(activeSlave) && !isAmputee(partnerSlave)) { - t += `${name} has ${partnerName} pushed against the wall is is fucking ${his} pussy from behind while {he2} struggles to get away.`; + t += `${name} has ${partnerName} pushed against the wall and is fucking ${his} pussy from behind while {he2} struggles to get away.`; } else if (fuckSeed > 70 && hasAnyArms(activeSlave)) { t += `${name} is on ${his} back and forcing ${partnerName} to ride ${his} dick while keeping a firm hold on ${his2} hips.`; } else if (fuckSeed > 60 && partnerSlave.belly < 500 && hasAnyLegs(activeSlave)) { diff --git a/src/interaction/sellSlave.js b/src/interaction/sellSlave.js index 48e4758eae3b4cfe1c43a85503b63aac3b7c170a..694efd89719e8bbd9f2a36e9a148452c3ea31f22 100644 --- a/src/interaction/sellSlave.js +++ b/src/interaction/sellSlave.js @@ -671,14 +671,16 @@ App.Interact.sellSlave = function(slave) { t.push(`I see ${he}'s branded with your mark; that won't have a significant impact.`); } } - let timeBeforeAgeRetirement; + let timeBeforeAgeRetirement = 52 - slave.birthWeek; if (V.policies.retirement.physicalAgePolicy === 1) { - timeBeforeAgeRetirement = 52 * (V.retirementAge - slave.physicalAge); + timeBeforeAgeRetirement += 52 * (V.retirementAge - (slave.physicalAge + 1)); } else { - timeBeforeAgeRetirement = 52 * (V.retirementAge - slave.actualAge); + timeBeforeAgeRetirement += 52 * (V.retirementAge - (slave.actualAge + 1)); } - if (slave.actualAge >= V.retirementAge - 5) { + if (slave.indenture > -1) { + t.push(`Though I dislike mentioning something so obvious, being an indentured servant will have a huge impact on ${his} valuation, especially since ${he} has just ${slave.indenture} weeks remaining on ${his} contract.`); + } else if (timeBeforeAgeRetirement < 260) { t.push(`Since ${he} has a mere ${timeBeforeAgeRetirement} weeks left until the local retirement age for sex slaves, buyers will be willing to offer much less for ${him}.`); } @@ -760,10 +762,6 @@ App.Interact.sellSlave = function(slave) { t.push(`And of course, you'll receive a premium for the nobility of such Imperial slaves.`); } - if (slave.indenture > -1) { - t.push(`Though I dislike mentioning something so obvious, being an indentured servant will have a huge impact on ${his} valuation.`); - } - switch (appraiser) { case "roman": t.push(`That is all." He rolls his screen-scroll up and tucks it into his toga, and nods. "A pleasure."`); diff --git a/src/js/sexActsJS.js b/src/js/sexActsJS.js index dafc34f40b3d532fad2664096d540e3abc6d5da9..363cc80c2086f8fc7f2d7243fa3c3a704aff3de9 100644 --- a/src/js/sexActsJS.js +++ b/src/js/sexActsJS.js @@ -236,6 +236,7 @@ globalThis.VCheck = (function() { } else if (canDoAnal(slave)) { return AnalVCheck(slave, times); } + return ""; } /** call as VCheck.Partner diff --git a/src/js/slaveCostJS.js b/src/js/slaveCostJS.js index 0c8b36dd1724db5088632bbff77ef71a91de1b6a..9caf19bb8e60eb6ec4643928cd1ef8d528b872a8 100644 --- a/src/js/slaveCostJS.js +++ b/src/js/slaveCostJS.js @@ -2460,7 +2460,7 @@ globalThis.slaveCostBeauty = function(slave, isStartingSlave, followLaws, isSpec } calcCareersCost(slave); calcMiscCost(slave); - calcIndentureCost(slave); /* multipliers */ + calcTimeRemainingCost(slave); /* multipliers */ calcCost(followLaws); if (isStartingSlave) { @@ -2611,7 +2611,7 @@ globalThis.slaveCostBeauty = function(slave, isStartingSlave, followLaws, isSpec } else if (slave.bellyPreg >= 120000) { updateMultiplier(`very preg`, -0.5); } else if (slave.bellyPreg >= 500 || slave.pregKnown === 1) { - updateMultiplier(`restart showing`, -0.1); + updateMultiplier(`baby bump`, -0.1); } } } @@ -2878,23 +2878,26 @@ globalThis.slaveCostBeauty = function(slave, isStartingSlave, followLaws, isSpec /** * @param {App.Entity.SlaveState} slave */ - function calcIndentureCost(slave) { + function calcTimeRemainingCost(slave) { + let weeksRemaining = 1000; + let weeksRemainingReason = `limited service`; if (slave.indenture > -1) { updateMultiplier(`indenture level`, -0.1 * slave.indentureRestrictions); - updateMultiplier(`indenture time`, -(260 - slave.indenture) / 260); - } else if (V.seeAge === 1 && slave.actualAge >= (V.retirementAge - 5)) { - /** - * replaced something like: - * multiplier *= (V.retirementAge - slave.actualAge) / 5; - * but allows us to save the intended difference to the multiplier for records, instead of modifying it directly - */ - const retireCalc = (tillRetire) => (multiplier * tillRetire / 5) - multiplier; + weeksRemainingReason = `indenture time`; + weeksRemaining = slave.indenture; + } else if (V.seeAge === 1) { + weeksRemaining = 52 - slave.birthWeek; // weeks to next birthday if (V.policies.retirement.physicalAgePolicy === 0) { - updateMultiplier(`near retirement`, retireCalc(V.retirementAge - slave.actualAge)); + weeksRemaining += (V.retirementAge - (slave.actualAge + 1)) * 52; + weeksRemainingReason = `near retirement (age)`; } else { - updateMultiplier(`near retirement`, retireCalc(V.retirementAge - slave.physicalAge)); + weeksRemaining += (V.retirementAge - (slave.physicalAge + 1)) * 52; + weeksRemainingReason = `near retirement (physical age)`; } } + if (weeksRemaining < 260) { // scale value linearly with less than five years remaining service + updateMultiplier(weeksRemainingReason, (multiplier * weeksRemaining / 260) - multiplier); + } } /** diff --git a/src/js/utilsPC.js b/src/js/utilsPC.js index 0edcd37e88586703dccab22c531da06667effcd0..fd1512a48e8b3f702ac935c98f4193fd3d3b8a81 100644 --- a/src/js/utilsPC.js +++ b/src/js/utilsPC.js @@ -959,6 +959,7 @@ globalThis.playerConsistencyCheck = function(actor = V.PC) { if (isHindered(actor)) { r += "+Hindered"; } + return r; } } else { return "Incapacitated"; diff --git a/src/npc/descriptions/belly/belly.js b/src/npc/descriptions/belly/belly.js index 7f56424ed59f2d6a9bb44b506566a1327f886739..86516a7773cbbc0b26c59c4305c0a1d167c8d12c 100644 --- a/src/npc/descriptions/belly/belly.js +++ b/src/npc/descriptions/belly/belly.js @@ -12113,9 +12113,9 @@ App.Desc.belly = function(slave, descType = DescType.NORMAL) { if (isBellyFluidLargest) { // TODO: write me } else if (slave.bellyImplant > 0) { - r.push(`${slave.slaveName}'s teddy is specially designed to accommodate such a absurdly swollen ${girl} and comes with a gap in the front for ${his} titanic implant-filled belly to bulge through.`); + r.push(`${slave.slaveName}'s teddy is specially designed to accommodate such an absurdly swollen ${girl} and comes with a gap in the front for ${his} titanic implant-filled belly to bulge through.`); } else { - r.push(`${slave.slaveName}'s teddy is specially designed to accommodate such a absurdly gravid ${girl} and comes with a gap in the front for ${his} titanic pregnant belly to spill out of.`); + r.push(`${slave.slaveName}'s teddy is specially designed to accommodate such an absurdly gravid ${girl} and comes with a gap in the front for ${his} titanic pregnant belly to spill out of.`); } } else if (slave.belly >= 450000) { if (isBellyFluidLargest) { diff --git a/src/npc/descriptions/describeBrands.js b/src/npc/descriptions/describeBrands.js index d81c3c66a12a050d32132c81e9e0c6d0f673e92a..d5a08f090aaedcbaef2725658f1ed794f3ca96f1 100644 --- a/src/npc/descriptions/describeBrands.js +++ b/src/npc/descriptions/describeBrands.js @@ -72,5 +72,6 @@ App.Desc.brand = function(slave, surface) { return `${rightBrand} branded into the flesh of ${his} ${surface.right}`; } } + return ``; } }; diff --git a/src/npc/descriptions/describePiercings.js b/src/npc/descriptions/describePiercings.js index 4bac4f12cde343780c5ffa0a1066ef0a927ee9de..137317f5f0f15217fb3a25d7e33058af4b325b7d 100644 --- a/src/npc/descriptions/describePiercings.js +++ b/src/npc/descriptions/describePiercings.js @@ -1,7 +1,7 @@ /** * @param {App.Entity.SlaveState} slave * @param {string} surface - * @returns {string} Relevant slave piercing, if present + * @returns {string|undefined} Relevant slave piercing, if present */ App.Desc.piercing = function(slave, surface) { "use strict"; @@ -10,7 +10,7 @@ App.Desc.piercing = function(slave, surface) { he, him, his, himself, girl, He, His } = getPronouns(slave); if (V.showBodyMods !== 1) { - return; + return undefined; } else if (slave.piercing[surface] && slave.piercing[surface].weight > 0 && slave.piercing[surface].desc) { return `${pronounsForSlaveProp(slave, slave.piercing[surface].desc)}.`; } diff --git a/src/npc/descriptions/describeTattoos.js b/src/npc/descriptions/describeTattoos.js index b821b8be6a8c5dbb4b7945761e88d074ac5f41ae..2205fd8a137259013e32cd48a083cf6dd702bc59 100644 --- a/src/npc/descriptions/describeTattoos.js +++ b/src/npc/descriptions/describeTattoos.js @@ -1,7 +1,7 @@ /** * @param {App.Entity.SlaveState} slave * @param {string} surface - * @returns {string} Relevant slave tattoo, if present + * @returns {string|undefined} Relevant slave tattoo, if present */ App.Desc.tattoo = function(slave, surface) { "use strict"; @@ -10,7 +10,7 @@ App.Desc.tattoo = function(slave, surface) { he, him, his, He, His, hers, himself } = getPronouns(slave); if (V.showBodyMods !== 1) { - return; + return undefined; } switch (surface) { case "shoulder": { diff --git a/src/npc/descriptions/descriptionWidgets.js b/src/npc/descriptions/descriptionWidgets.js index 89ebd2beddb6fcfb91895a2826c22d7fab0dcaba..14da986554939d38cc2590ef40df0558512f5f0a 100644 --- a/src/npc/descriptions/descriptionWidgets.js +++ b/src/npc/descriptions/descriptionWidgets.js @@ -823,11 +823,11 @@ App.Desc.ageAndHealth = function(slave) { /** * @param {App.Entity.SlaveState} slave * @returns {string} Slave's mods. - * @param {string} surface + * @param {string|undefined} surface */ App.Desc.mods = function(slave, surface) { if (V.showBodyMods !== 1) { - return; + return undefined; } if (slave.fuckdoll !== 0 && !["anus", "lips", "vagina"].includes(surface)) { /* Fuckdoll vulva and anus alone are visible, plus enormous lips */ return App.Desc.piercing(slave, surface); // Most piercings are part of the suit and have appropriate descriptions @@ -921,7 +921,7 @@ App.Desc.limbs = function(slave) { if (hasAnyQuadrupedLimbs(slave) && !(getLeftArmID(slave) === getRightArmID(slave) && getLeftArmID(slave) === getLeftLegID(slave) && getLeftArmID(slave) === getRightLegID(slave))){ - r += `The nature of ${his} prosthetics force ${him} to walk like an quadrupedal animal.`; + r += `The nature of ${his} prosthetics force ${him} to walk like a quadrupedal animal.`; } return r; /* diff --git a/src/npc/descriptions/ears.js b/src/npc/descriptions/ears.js index c009f8e22d69c0bc967a582a4117f18dc85ab4f2..62f293ffe1b3e2d2e2f6640a1810de0841208b11 100644 --- a/src/npc/descriptions/ears.js +++ b/src/npc/descriptions/ears.js @@ -41,7 +41,7 @@ App.Desc.ears = function(slave) { } else if (slave.earShape === "orcish") { r.push(`${He} has small, pointed orcish ears.`); } else if (slave.earShape === "cow") { - r.push(`${His} long, floppy ${App.Utils.translate("cow")} ears are adorably endearing and give ${him} a innocuous appearance. ${His} ears seem to be very sensitive to touch.`); // that ${either(`tend to droop when ${he} is relaxed or sad`, `tend waggle up and down when ${he} is excited`, `twitch at the slightest touch`)}. These don't make sense for the most part. + r.push(`${His} long, floppy ${App.Utils.translate("cow")} ears are adorably endearing and give ${him} an innocuous appearance. ${His} ears seem to be very sensitive to touch.`); // that ${either(`tend to droop when ${he} is relaxed or sad`, `tend waggle up and down when ${he} is excited`, `twitch at the slightest touch`)}. These don't make sense for the most part. } else if (slave.earShape === "sheep") { r.push(`${His} cupped ${slave.hColor} colored wooly sheep ears are incredibly soft and adorable. ${His} ears seem to be very sensitive to touch.`); } else if (slave.earShape === "gazelle") { diff --git a/src/npc/descriptions/legs.js b/src/npc/descriptions/legs.js index 87d7d5bb81770228e28c434e18b72d228eca7edc..2860de9e5874d309905b679a8fb7ac59318fc39e 100644 --- a/src/npc/descriptions/legs.js +++ b/src/npc/descriptions/legs.js @@ -70,4 +70,6 @@ App.Desc.legs = function(slave) { } } } + + return ``; }; diff --git a/src/npc/descriptions/style/clothingCorset.js b/src/npc/descriptions/style/clothingCorset.js index d9620067f955214e589a9ffe96c82ee0579ed05e..675d350ea2e5d25c572d9c8ff2a0b172a5c71047 100644 --- a/src/npc/descriptions/style/clothingCorset.js +++ b/src/npc/descriptions/style/clothingCorset.js @@ -90,7 +90,7 @@ App.Desc.clothingCorset = function(slave) { } else if (slave.bellyAccessory === "an extreme corset") { r.push(`${His} extreme corsetage is visible through the sides.`); } else if (slave.bellyAccessory === "a support band") { - r.push(`${His} support band is is visible through the sides.`); + r.push(`${His} support band is visible through the sides.`); } break; case "a slutty qipao": diff --git a/src/npc/descriptions/style/footwear.js b/src/npc/descriptions/style/footwear.js index 2e8ad428531cfb5907f555f9058c3ce01a3d6ba4..265c40cedd189eb689a43660060ddcc97dded616 100644 --- a/src/npc/descriptions/style/footwear.js +++ b/src/npc/descriptions/style/footwear.js @@ -217,7 +217,7 @@ App.Desc.footwear = function(slave) { if (bothFeet) { r.push(`aside from a pair of utilitarian leather boots.`); } else { - r.push(`aside from an utilitarian leather boot.`); + r.push(`aside from a utilitarian leather boot.`); } break; case "heels": @@ -2130,7 +2130,7 @@ App.Desc.footwear = function(slave) { if (bothFeet) { r.push(`a pair of flat shoes with decorative bows.`); } else { - r.push(`a flat shoe with an decorative bow.`); + r.push(`a flat shoe with a decorative bow.`); } break; case "boots": @@ -3550,7 +3550,7 @@ App.Desc.footwear = function(slave) { if (bothFeet) { r.push(`sport a pair of immodestly tall black heels.`); } else { - r.push(`sports a immodestly tall black heel.`); + r.push(`sports an immodestly tall black heel.`); } break; case "platform shoes": @@ -3571,7 +3571,7 @@ App.Desc.footwear = function(slave) { if (bothFeet) { r.push(`sport a pair of immodestly tall black platform heels.`); } else { - r.push(`sports a immodestly tall black platform heel.`); + r.push(`sports an immodestly tall black platform heel.`); } break; default: @@ -4327,7 +4327,7 @@ App.Desc.footwear = function(slave) { if (bothFeet) { r.push(`crisscross ${his} thighs and calves down to a pair of golden heels.`); } else { - r.push(`crisscross ${his} thigh and calf down to aa golden heel.`); + r.push(`crisscross ${his} thigh and calf down to a golden heel.`); } break; case "pumps": diff --git a/src/npc/descriptions/waist.js b/src/npc/descriptions/waist.js index 50fed8e48f85ece782b1df4b922ca383e306dd81..dea6736984354bef929728cf1bec955154488275 100644 --- a/src/npc/descriptions/waist.js +++ b/src/npc/descriptions/waist.js @@ -149,7 +149,7 @@ App.Desc.waist = function(slave) { } r.push(...normalWaistBelly()); } else if (slave.waist >= -95) { - r.push(`a hot <span class="pink">wasp waist</span> that gives ${him} an hourglass`); + r.push(`a hot <span class="pink">wasp waist</span> that gives ${him} a hourglass`); if (slave.weight > 30) { r.push(`figure despite ${his} extra weight.`); } else if (slave.weight < -30) { diff --git a/src/npc/generate/generateNewSlaveJS.js b/src/npc/generate/generateNewSlaveJS.js index 2921544c7fee615fbb5e4264cde3cb88e8a92fdf..9bc98f76d9d5b33b1ea1237f96a1277236be3793 100644 --- a/src/npc/generate/generateNewSlaveJS.js +++ b/src/npc/generate/generateNewSlaveJS.js @@ -47,7 +47,7 @@ globalThis.GenerateNewSlave = (function() { /** * @returns {App.Entity.SlaveState} * @param {"XY"|"XX"|""} [sex] null or omit to use default rules - * @param {GenerateNewSlavePram|Object} [Obj] + * @param {GenerateNewSlavePram|object} [Obj] */ function GenerateNewSlave(sex, { minAge, diff --git a/src/npc/generate/heroCreator.js b/src/npc/generate/heroCreator.js index bc94d06acc9d182b7dc65a713d22dbd3fa4a6326..1d2972592ee7b204d7a1292ef7f45c3a94d9dddc 100644 --- a/src/npc/generate/heroCreator.js +++ b/src/npc/generate/heroCreator.js @@ -185,7 +185,7 @@ App.Utils.getHeroSlave = function(heroSlave) { /** * Marks limbs to be removed when going trough App.Utils.getHeroSlave. * Does not actually remove limbs, only use on slaves that go through App.Utils.getHeroSlave!! - * @param {Object} hero + * @param {object} hero * @param {FC.LimbArgumentAll} [limb="all"] */ App.Utils.removeHeroLimbs = function(hero, limb = "all") { diff --git a/src/npc/generate/newChildIntro.js b/src/npc/generate/newChildIntro.js index 25cdd4523005b00d62f1ba8de1e4439d4fc2f18f..8f5fd6274d3dbc53f5bcc0e0b6289967f4abdda1 100644 --- a/src/npc/generate/newChildIntro.js +++ b/src/npc/generate/newChildIntro.js @@ -49,7 +49,9 @@ App.UI.newChildIntro = function(slave) { if (slave.preg > 0) { /* Unused for now. Fetal development would be accelerated as well. As a result, the released slave would be shocking to see in such a state. */ if (slave.geneticQuirks.progeria) { + // unused for now } else if (slave.geneticQuirks.neoteny && slave.actualAge > 12 && V.geneticMappingUpgrade < 2) { + // unused for now } } else if (slave.geneticQuirks.progeria && V.geneticMappingUpgrade < 2) { r.push(`you barely manage to pull yourself together to catch ${him} in time. There must have been some mistake with the settings; ${he} should not be <i>this</i> old. You help ${him} to ${his} unstable feet and slowly walk ${him} to your penthouse.`); diff --git a/src/npc/generate/newSlaveIntro.js b/src/npc/generate/newSlaveIntro.js index eca70abc97149e86cb74c86223c853cc7fd20fa8..cc5e74c7acd3df72abbfb013c6990e5f46d141e9 100644 --- a/src/npc/generate/newSlaveIntro.js +++ b/src/npc/generate/newSlaveIntro.js @@ -1,7 +1,7 @@ /** * @param {FC.GingeredSlave} slave * @param {App.Entity.SlaveState} [slave2] recruiter slave, if present in the scene - * @param {Object} [obj] + * @param {object} [obj] * @param {boolean} [obj.tankBorn] * @param {string} [obj.momInterest] * @param {string} [obj.dadInterest] diff --git a/src/npc/generateSlaveBot.js b/src/npc/generateSlaveBot.js index 386ab27b319f82adeb0b3aab51a3d5f0e282bdf6..69b50a1184466256b15efd38b2dc9b27c82b11d3 100644 --- a/src/npc/generateSlaveBot.js +++ b/src/npc/generateSlaveBot.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /** @param {App.Entity.SlaveState} slave */ App.UI.SlaveInteract.createSlaveBot = function(slave) { const el = new DocumentFragment(); @@ -8,7 +9,6 @@ App.UI.SlaveInteract.createSlaveBot = function(slave) { // Adapted from https://github.com/ZoltanAI/character-editor class Exporter { - static downloadFile(file) { const link = window.URL.createObjectURL(file); @@ -19,7 +19,7 @@ class Exporter { } static Json(characterCard) { - const file = new File([JSON.stringify(characterCard, undefined, '\t')], (characterCard.data.name || 'character') + '.json', { type: 'application/json;charset=utf-8' }); + const file = new File([JSON.stringify(characterCard, undefined, '\t')], (characterCard.data.name || 'character') + '.json', {type: 'application/json;charset=utf-8'}); Exporter.downloadFile(file); } @@ -27,41 +27,41 @@ class Exporter { /** @param {App.Entity.SlaveState} slave */ function createCharacterDataFromSlave(slave) { - // Construct a character card based on the Card v2 spec: https://github.com/malfoyslastname/character-card-spec-v2 - var characterCard = { - spec: 'chara_card_v2', - spec_version: '2.0', // May 8th addition - data: { + // Construct a character card based on the Card v2 spec: https://github.com/malfoyslastname/character-card-spec-v2 + const characterCard = { + spec: 'chara_card_v2', + spec_version: '2.0', // May 8th addition + data: { alternate_greetings: [], avatar: "none", character_version: "main", creator: `FreeCities ${App.Version.pmod} System Generated`, creator_notes: `FreeCities ${App.Version.pmod} System Generated Slave Bot`, - description: generateDescription(slave) + generatePromptStd(slave), - first_mes: generateFirstMessage(slave), + description: generateDescription(slave) + generatePromptStd(slave), + first_mes: generateFirstMessage(slave), mes_example: "", name: slave.slaveName, personality: generatePersonality(slave), - scenario: `{{char}} and {{user}} exist in the slaveholding arcology of ${V.arcologies[0].name}. {{char}} is in {{user}}'s office, waiting for inspection.`, - system_prompt: "", + scenario: `{{char}} and {{user}} exist in the slaveholding arcology of ${V.arcologies[0].name}. {{char}} is in {{user}}'s office, waiting for inspection.`, + system_prompt: "", character_book: { // aka Lorebook - entries: [ - lorebookArcology(0), - lorebookFuckdoll(1), - lorebookMindbroken(2), - lorebookArcade(3), - lorebookDairy(4) + entries: [ + lorebookArcology(0), + lorebookFuckdoll(1), + lorebookMindbroken(2), + lorebookArcade(3), + lorebookDairy(4) // lorebookActiveFS(100), // adds all active FS starting at (n) ], name: `Free Cities ${slave.slaveName}` - } + } } - } - return characterCard + }; + return characterCard; } function generateDescription(slave) { - let r = []; + let r = []; const {He, His, he, him, his} = getPronouns(slave); let descParts = []; @@ -69,14 +69,14 @@ function generateDescription(slave) { // r.push(lorebookActiveFS()); // NAME - r.push(`Name: ${SlaveFullName(slave)}`); - - //RELATIONSHIP - r.push("\r\nRelationship: {{char}} is {{user}}'s "); + r.push(`Name: ${SlaveFullName(slave)}`); + + // RELATIONSHIP + r.push("\r\nRelationship: {{char}} is {{user}}'s "); // Slave age (consider visible age) r.push(`${slave.actualAge} year old `); - - // Devotion + + // Devotion if (slave.fetish !== Fetish.MINDBROKEN) { if (slave.devotion < -95) { r.push("hate-filled, "); @@ -104,11 +104,13 @@ function generateDescription(slave) { } else if (slave.trust < 20) { r.push("fearful "); } else if (slave.trust <= 50) { - if (slave.devotion < -20) { - r.push("careful "); - } else { - r.push("careful "); - } + // if (slave.devotion < -20) { + // r.push("careful "); + // } else { + // r.push("careful "); + // } + // FIXME: @null this block always spits out the same result. It should be changed or removed + r.push("careful "); } else if (slave.trust < 95) { if (slave.devotion < -20) { r.push("bold "); @@ -126,19 +128,19 @@ function generateDescription(slave) { r.push("mindbroken "); } - // Slave's Title, ex:"pregnant big bottomed busty milky hourglass broodmother" + // Slave's Title, ex:"pregnant big bottomed busty milky hourglass broodmother" if (slave.fuckdoll > 0) { r.push("Fuckdoll"); } else { r.push(`${SlaveTitle(slave)}`); } - + // DESCRIPTION r.push("\r\nDescription: "); - + // Eyes // eye color (orig vs. current?), Add check for no eyes (does it matter?) - + if (slave.fuckdoll > 0) { r.push(`blinded, `); } else if (!canSee(slave)) { @@ -152,7 +154,7 @@ function generateDescription(slave) { } else { // Skin r.push(`${slave.skin} skin, `); - + // Slave intelligence: Ignore average, include mindbroken if (slave.fetish === Fetish.MINDBROKEN) { r.push("mindbroken, "); @@ -165,96 +167,96 @@ function generateDescription(slave) { } else if (slave.intelligence > 50) { r.push("very smart, "); } - + // Beauty if (slave.face < -40) { - r.push(`${slave.faceShape} ugly face, `); - } else if (slave.face > 50) { - r.push(`${slave.faceShape} gorgeous face, `); - } else if (slave.face > 10) { - r.push(`${slave.faceShape} very pretty face, `); - } else { - r.push(`${slave.faceShape} face, `); - } + r.push(`${slave.faceShape} ugly face, `); + } else if (slave.face > 50) { + r.push(`${slave.faceShape} gorgeous face, `); + } else if (slave.face > 10) { + r.push(`${slave.faceShape} very pretty face, `); + } else { + r.push(`${slave.faceShape} face, `); + } // Hairstyle if (slave.hLength > 100) { - r.push("very long ") + r.push("very long "); } else if (slave.hLength > 30) { - r.push("long ") + r.push("long "); } else if (slave.hLength > 10) { - r.push("short ") + r.push("short "); } else if (slave.hLength > 0) { - r.push("very short ") + r.push("very short "); } - r.push(`${slave.hColor} `) + r.push(`${slave.hColor} `); // Add "hair" to hairstyles that need it to make sense (e.g. "messy" becomes "messy hair" but "dreadlocks" stays as is) if (["braided", "curled", "eary", "bun", "messy bun", "tails", "drills", "luxurious", "messy", "neat", "permed", "bangs", "hime", "strip", "up", "trimmed", "undercut", "double buns", "chignon"].includes(slave.hStyle)) { - r.push(`${slave.hStyle} hair, `) + r.push(`${slave.hStyle} hair, `); } else { - r.push(`${slave.hStyle}, `) + r.push(`${slave.hStyle}, `); } - + // Start conditional descriptions - // Eductation (bimbo/hindered, well educated) + // Education (bimbo/hindered, well educated) if (slave.education < -10) { - descParts.push("vapid bimbo with no education"); + descParts.push("vapid bimbo with no education"); } else if (slave.intelligence > 25) { - descParts.push("very well educated"); - } - } + descParts.push("very well educated"); + } + } // Height. Ignore Average if (slave.height < 150) { - descParts.push(`short`); - } else if (slave.height > 180) { - descParts.push(`tall`); - } - - // Weight. Ignore average + descParts.push(`short`); + } else if (slave.height > 180) { + descParts.push(`tall`); + } + + // Weight. Ignore average if (slave.weight < -95) { - descParts.push(`emaciated`); - } else if (slave.weight < -30) { - descParts.push(`very skinny`); - } else if (slave.weight > 95) { - descParts.push(`very fat`); - } else if (slave.weight > 30) { - descParts.push(`plump`); - } - + descParts.push(`emaciated`); + } else if (slave.weight < -30) { + descParts.push(`very skinny`); + } else if (slave.weight > 95) { + descParts.push(`very fat`); + } else if (slave.weight > 30) { + descParts.push(`plump`); + } + // Boobs. Ignore Average. Add lactation? NG if (slave.boobs < 300) { - descParts.push(`flat chested`); - } else if (slave.boobs < 500) { - descParts.push(`small breasts`); - } else if (slave.boobs > 1400) { - descParts.push(`massive breasts that impede movement`); - } else if (slave.boobs > 800) { - descParts.push(`large breasts`); - } - + descParts.push(`flat chested`); + } else if (slave.boobs < 500) { + descParts.push(`small breasts`); + } else if (slave.boobs > 1400) { + descParts.push(`massive breasts that impede movement`); + } else if (slave.boobs > 800) { + descParts.push(`large breasts`); + } + // Butt. Ignore average if (slave.butt <= 1) { - descParts.push(`flat butt`); - } else if (slave.butt > 7) { - descParts.push(`gigantic ass`); - } else if (slave.butt > 3) { - descParts.push(`big ass`); - } - + descParts.push(`flat butt`); + } else if (slave.butt > 7) { + descParts.push(`gigantic ass`); + } else if (slave.butt > 3) { + descParts.push(`big ass`); + } + // Musculature if (slave.muscles < -31) { - descParts.push(`very weak`); - } else if (slave.muscles > 50) { - descParts.push(`very muscular`); - } - + descParts.push(`very weak`); + } else if (slave.muscles > 50) { + descParts.push(`very muscular`); + } + // Check amputee (add missing just arms/legs) if (isAmputee(slave)) { descParts.push(`missing both arms and both legs`); } - + // Check pregnant if (slave.preg > 30) { descParts.push(`very pregnant`); @@ -277,6 +279,9 @@ function generateDescription(slave) { // BACKGROUND if (slave.fuckdoll > 0 || slave.fetish === Fetish.MINDBROKEN) { // in neither case would slave recall or this be important null; + // FIXME: @null Is this supposed to be a return? or is this supposed to do nothing? + // If nothing it should be a single if statement, not if else. `if (slave.fuckdoll <== 0 || slave.fetish !== Fetish.MINDBROKEN) {` + // If it's a place holder replace null with a `// TODO: thing to do` comment } else { if (slave.career === "a slave") { r.push(`\r\nBackground: {{char}} has been enslaved for as long as ${he} can remember`); @@ -287,11 +292,14 @@ function generateDescription(slave) { // ASSIGNMENT r.push(`\r\nAssignment: ${slave.assignment}`); - + if (slave.fuckdoll > 0) { null; + // FIXME: @null Is this supposed to be a return? or is this supposed to do nothing? + // If nothing it should be a single if statement, not if else. `if (slave.fuckdoll <== 0) {` + // If it's a place holder replace null with a `// TODO: thing to do` comment } else { - // FETISH + // FETISH // Paraphilias listed and prompted if (slave.sexualFlaw === SexualFlaw.CUMADDICT ) { r.push(`\r\nFetish: pathologically addicted to cum`); @@ -300,17 +308,17 @@ function generateDescription(slave) { } else if (slave.sexualFlaw === SexualFlaw.NEGLECT ) { r.push(`\r\nTrait: only considers ${his} partner's pleasure`); } else if (slave.sexualFlaw === SexualFlaw.ATTENTION ) { - r.push(`\r\nTrait: pathologically narcissistic`); + r.push(`\r\nTrait: pathologically narcissistic`); } else if (slave.sexualFlaw === SexualFlaw.BREASTEXP ) { r.push(`\r\nFetish: pathologically addicted to breast augmentation`); // is this right } else if (slave.sexualFlaw === SexualFlaw.SELFHATING ) { r.push(`\r\nTrait: pathologically masochistic`); } else if (slave.sexualFlaw === SexualFlaw.ABUSIVE || slave.sexualFlaw === SexualFlaw.MALICIOUS) { - r.push(`\r\nTrait: sociopathic, delights in abusing others`); //Are above the same for purposes of LLM + r.push(`\r\nTrait: sociopathic, delights in abusing others`); // Are above the same for purposes of LLM } - - // Explain sex/entertainment skill level. Leave off average. Check virgin status. - + + // Explain sex/entertainment skill level. Leave off average. Check virgin status. + // ABILITIES let abilParts = []; if (slave.vagina === 0){ @@ -322,7 +330,7 @@ function generateDescription(slave) { } else if (slave.skill.whoring > 61){ abilParts.push("expert whore"); } - + if (slave.skill.entertainment > 100){ abilParts.push(`renowned entertainer`); } else if (slave.skill.entertainment > 61){ @@ -344,7 +352,7 @@ function generateDescription(slave) { r.push("\r\nGenitals: "); if (slave.dick === 0 && slave.vagina === -1) { // null slave r.push("No genitals"); - } else if (slave.vagina < 0) { // has a dick + } else if (slave.vagina < 0) { // has a dick if (slave.dick < 1) { r.push("tiny penis"); } else if (slave.dick > 8) { @@ -366,23 +374,25 @@ function generateDescription(slave) { r.push("pussy"); } // Back - if (slave.anus === 0) { + if (slave.anus === 0) { r.push(", virgin anus"); } else if (slave.vagina > 3) { r.push(", loose anus"); - } + } // PHair - if (slave.pubicHStyle === "hairless" || slave.pubicHStyle === "bald") { + if (slave.pubicHStyle === "hairless" || slave.pubicHStyle === "bald") { r.push(", no pubic hair"); } else { r.push(`, pubic hair ${slave.pubicHStyle}`); - } - + } // RULES let rulesParts = []; if (slave.fuckdoll > 0) { null; + // FIXME: @null Is this supposed to be a return? or is this supposed to do nothing? + // If nothing it should be a single if statement, not if else. `if (slave.fuckdoll <== 0) {` + // If it's a place holder replace null with a `// TODO: thing to do` comment } else { // Speech (also check for mute) if (slave.voice === 0) { @@ -396,20 +406,22 @@ function generateDescription(slave) { } // How they address the user, if not mute if (slave.voice !== 0) { - rulesParts.push(`Addresses {{user}} as ${properMaster()}`) + rulesParts.push(`Addresses {{user}} as ${properMaster()}`); } - } - + } + if (rulesParts.length > 0) { r.push(`\r\nRules: ${rulesParts.join(', ')}`); } - // TATTOOS - Too much context for impact? let tattooParts = []; if (slave.fuckdoll > 0) { null; + // FIXME: @null Is this supposed to be a return? or is this supposed to do nothing? + // If nothing it should be a single if statement, not if else. `if (slave.fuckdoll <== 0) {` + // If it's a place holder replace null with a `// TODO: thing to do` comment } else { if (slave.armsTat) { tattooParts.push(`${slave.armsTat} arm tattoo`); @@ -420,7 +432,7 @@ function generateDescription(slave) { if (slave.bellyTat) { tattooParts.push(`${slave.bellyTat} belly tattoo`); } - if (slave.boobsTat) { + if (slave.boobsTat) { tattooParts.push(`${slave.boobsTat} breast tattoo`); } @@ -433,14 +445,13 @@ function generateDescription(slave) { let chasParts = []; if (slave.chastityVagina === 1) { chasParts.push(`vagina`); - } + } if (slave.chastityPenis === 1) { chasParts.push(`penis`); - } + } if (slave.chastityAnus === 1) { chasParts.push(`anus`); - } - + } if (chasParts.length > 0) { r.push(`\r\nChastity device covers: ${chasParts.join(', ')}`); } @@ -458,13 +469,16 @@ function generateDescription(slave) { // Lover if (slave.fuckdoll > 0) { null; + // FIXME: @null Is this supposed to be a return? or is this supposed to do nothing? + // If nothing it should be a single if statement, not if else. `if (slave.fuckdoll <== 0) {` + // If it's a place holder replace null with a `// TODO: thing to do` comment } else { const lover = slave.relationship > 0 ? getSlave(slave.relationshipTarget) : null; if (lover) { if (slave.relationship > 4) { r.push(`\r\n${SlaveFullName(lover)} is {{char}}'s wife`); } else if (slave.relationship > 3) { - r.push(`\r\n${SlaveFullName(lover)} is {{char}}'s lover`); + r.push(`\r\n${SlaveFullName(lover)} is {{char}}'s lover`); } else if (slave.relationship > 2) { r.push(`\r\n${SlaveFullName(lover)} is {{char}}'s girlfriend`); } else if (slave.relationship > 1) { @@ -478,6 +492,9 @@ function generateDescription(slave) { // Rival if (slave.fuckdoll > 0) { null; + // FIXME: @null Is this supposed to be a return? or is this supposed to do nothing? + // If nothing it should be a single if statement, not if else. `if (slave.fuckdoll <== 0) {` + // If it's a place holder replace null with a `// TODO: thing to do` comment } else { const rival = slave.rivalry > 0 ? getSlave(slave.rivalryTarget) : null; if (rival) { @@ -490,12 +507,12 @@ function generateDescription(slave) { } } } - - return r.join(""); + + return r.join(""); } function generatePromptStd(slave) { - let r = [] + let r = []; // PERMANENT CHAR PROMPT PART - Does not rely on any slave attributes, but leaving with .join and (slave) passthrough for future conditionals r.push(`\r\n{{user}} is the owner of ${V.arcologies[0].name}, an arcology in the fictional slave-holding world of Free Cities. `); @@ -506,11 +523,10 @@ function generatePromptStd(slave) { } function generateFirstMessage(slave) { - let r = [] + let r = []; const {He, His, he, him, his} = getPronouns(slave); - // Set up basic scenario - r.push("I am sitting in my office as {{char}} arrives for inspection.\r\n") + r.push("I am sitting in my office as {{char}} arrives for inspection.\r\n"); // Trust switches if (slave.fuckdoll > 0) { @@ -521,7 +537,7 @@ function generateFirstMessage(slave) { r.push(`${He} is eerily still. `); } } else { - r.push("{{char}} comes in, ") + r.push("{{char}} comes in, "); if (slave.trust < -95) { r.push(`appearing abjectly terrified, barely able to control ${his} terror.`); @@ -532,11 +548,13 @@ function generateFirstMessage(slave) { } else if (slave.trust < 20) { r.push("looking fearful. "); } else if (slave.trust <= 50) { - if (slave.devotion < -20) { - r.push("appearing cautious. "); - } else { - r.push("appearing cautious. "); - } + // if (slave.devotion < -20) { + // r.push("appearing cautious. "); + // } else { + // r.push("appearing cautious. "); + // } + // FIXME: @null this block always spits out the same result. It should be changed or removed + r.push("appearing cautious. "); } else if (slave.trust < 95) { if (slave.devotion < -20) { r.push("boldly approaching my desk. "); @@ -550,7 +568,7 @@ function generateFirstMessage(slave) { r.push(`a serene look of profound trust on ${his} face. `); } } - + // Devotion switches if (slave.devotion < -95) { r.push(`${His} hate-filled face is barely under control. `); @@ -567,28 +585,26 @@ function generateFirstMessage(slave) { } else { r.push(`${He} is positively glowing at the opportunity to have ${his} precious ${properMaster()}'s attention, radiating worshipful devotion.`); } - - } + } r.push("\r\n"); // Check Voice and Vocal Rules - if (slave.voice === 0 || slave.rules.speech === "restrictive") { + if (slave.voice === 0 || slave.rules.speech === "restrictive") { r.push(`{{char}} is silent, waiting for {{user}} to act.`); } else { r.push(`{{char}} looks forward and speaks, "${properMaster()}, how may I serve you?"`); } return r.join(""); - } function generatePersonality(slave) { - let r = [] + let r = []; r.push("\r\n{{char}} is {{user}}'s "); // Slave age (consider visible age) r.push(`${slave.actualAge} year old `); - - // Devotion + + // Devotion if (slave.fetish !== Fetish.MINDBROKEN) { if (slave.devotion < -95) { r.push("hate-filled, "); @@ -616,11 +632,13 @@ function generatePersonality(slave) { } else if (slave.trust < 20) { r.push("fearful "); } else if (slave.trust <= 50) { - if (slave.devotion < -20) { - r.push("careful "); - } else { - r.push("careful "); - } + // if (slave.devotion < -20) { + // r.push("careful "); + // } else { + // r.push("careful "); + // } + // FIXME: @null this block always spits out the same result. It should be changed or removed + r.push("careful "); } else if (slave.trust < 95) { if (slave.devotion < -20) { r.push("bold "); @@ -635,12 +653,12 @@ function generatePersonality(slave) { } } } else { - r.push("mindbroken "); + r.push("mindbroken "); } - // Slave's Title + // Slave's Title if (slave.fuckdoll > 0) { - r.push("Fuckdoll"); + r.push("Fuckdoll"); } else { r.push(`${SlaveTitle(slave)}`); } @@ -652,9 +670,9 @@ function generatePlayerSummary() { // sections here taken from pLongDescription.js const PC = V.PC; const raceA = ["asian", "amerindian", "indo-aryan"].includes(PC.race) ? "an" : "a"; // addA() was inheriting colors. - const r = [] - - r.push(`{{user}} is `) + const r = []; + + r.push(`{{user}} is `); if (!PC.nationality || V.seeNationality !== 1 || PC.nationality === "Stateless" || PC.nationality === "slave") { r.push(`${PC.race} `); } else if (PC.nationality === "Zimbabwean" && PC.race === "white") { @@ -688,8 +706,8 @@ function generatePlayerSummary() { ageDifference = `{{user}} has taken measures to look a younger ${PC.visualAge}.`; } r.push(`{{user}} is ${PC.actualAge} years old.`, ageDifference); - - return r.join("") + + return r.join(""); } function lorebookArcology(_n) { // First entry is primarily an example @@ -697,7 +715,7 @@ function lorebookArcology(_n) { // First entry is primarily an example id: _n, keys: ["arcology"], // Trigger word for ST to use the content secondary_keys: [], // Second Trigger condition (requires both to insert) - comment: "", + comment: "", content: "Arcology: sustainable urban living complex, integrating living spaces, workspaces, and recreational areas in a harmonious and efficient manner. ", // This content is inserted in context. Note it includes Key constant: false, selective: true, @@ -714,7 +732,7 @@ function lorebookArcology(_n) { // First entry is primarily an example selectiveLogic: 0, group: "" } - } + }; } function lorebookFuckdoll(_n) { @@ -739,11 +757,11 @@ function lorebookFuckdoll(_n) { selectiveLogic: 0, group: "" } - } + }; } function lorebookMindbroken(_n) { - return{ + return { id: _n, keys: ["mindbroken"], secondary_keys: [], @@ -764,16 +782,16 @@ function lorebookMindbroken(_n) { selectiveLogic: 0, group: "" } - } + }; } function lorebookArcade(_n) { - return{ + return { id: _n, keys: ["arcade"], secondary_keys: [], comment: "", - content: "Arcade: gloryhole stalls, offering retrained slaves mouths, vaginas and anuses for use by the public", + content: "Arcade: gloryhole stalls, offering retrained slaves mouths, vaginas and anuses for use by the public", constant: false, selective: true, insertion_order: 100, @@ -789,7 +807,7 @@ function lorebookArcade(_n) { selectiveLogic: 0, group: "" } - } + }; } function lorebookDairy(_n) { @@ -798,7 +816,7 @@ function lorebookDairy(_n) { keys: ["dairy"], secondary_keys: [], comment: "", - content: "Dairy: barn used for milking and breeding human slaves, collecting human milk for sale and selling breeding services", + content: "Dairy: barn used for milking and breeding human slaves, collecting human milk for sale and selling breeding services", constant: false, selective: true, insertion_order: 100, @@ -814,5 +832,5 @@ function lorebookDairy(_n) { selectiveLogic: 0, group: "" } - } + }; } diff --git a/src/npc/interaction/FSuckle.js b/src/npc/interaction/FSuckle.js index bec8e22614404262a778579203dd10b106365479..a0bb06cc204846467a938f469b9406b8f71c435c 100644 --- a/src/npc/interaction/FSuckle.js +++ b/src/npc/interaction/FSuckle.js @@ -631,7 +631,7 @@ App.Interact.fSuckle = function(slave) { r.push(`as hard as you can.`); } } else if (slave.boobs >= 2000) { - r.push(`While you were busy suckling, ${he} was anything but idle, using ${his} hand as best as ${he} could to bring you the most pleasure ${he} is is capable of. Though no lube was applied, ${his} ministrations were more than enough to take you to the knife's edge of orgasm more than once. ${His} hand continues to apply itself to your`); + r.push(`While you were busy suckling, ${he} was anything but idle, using ${his} hand as best as ${he} could to bring you the most pleasure ${he} is capable of. Though no lube was applied, ${his} ministrations were more than enough to take you to the knife's edge of orgasm more than once. ${His} hand continues to apply itself to your`); if (V.PC.dick !== 0) { r.push(`shaft, its fingertips brushing across your shaft to tickle its head and make you thrust instinctively. ${His} tugs,`); } else { diff --git a/src/npc/startingGirls/startingGirls.js b/src/npc/startingGirls/startingGirls.js index c0b2306c72e7423c5961d87cbefc92c68b8550e4..0a7b3a3a38074925ddb8da2ca07b9a0343f6e271 100644 --- a/src/npc/startingGirls/startingGirls.js +++ b/src/npc/startingGirls/startingGirls.js @@ -663,7 +663,7 @@ App.StartingGirls.playerOrigin = function(slave) { }; /** - * @param {Object} option + * @param {object} option * @param {Array<startingGirlsOptionsPreset>} set */ App.StartingGirls.addSet = function(option, set) { @@ -1636,7 +1636,7 @@ App.StartingGirls.careerFilter = "Any"; /** @type {Map<string, function(string): boolean>} */ App.StartingGirls.careerBonusFilters = (function() { /** - * @param {Object} obj + * @param {object} obj * @param {string} key * @returns {[string, function(string): boolean]} */ diff --git a/src/npc/startingGirls/startingGirlsPassage.js b/src/npc/startingGirls/startingGirlsPassage.js index 0a9908a0df9e037fb908c5e43a8af03c9ee86348..30cb464acd9d935da4a9cae4b07b0c69f9d50bd7 100644 --- a/src/npc/startingGirls/startingGirlsPassage.js +++ b/src/npc/startingGirls/startingGirlsPassage.js @@ -76,6 +76,51 @@ App.StartingGirls.passage = function() { ) ); + class SlaveExporterImporter { + constructor(textareaElement) { + this.textareaToExportTo = textareaElement; + } + exportToTextarea(string) { + this.textareaToExportTo.value = string; + } + exportActiveSlave() { + this.textareaToExportTo.value = JSON.stringify(V.activeSlave); + } + importActiveSlave() { + const slave = JSON.parse(this.textareaToExportTo.value); + App.Update.Slave(slave); + App.Entity.Utils.SlaveDataSchemeCleanup(slave); + SlaveDatatypeCleanup(slave); + removeJob(slave, slave.assignment); + V.activeSlave = slave; + } + } + const textareaElement = App.UI.DOM.appendNewElement("textarea", el, JSON.stringify(V.activeSlave, null, 2)); + const exporter = new SlaveExporterImporter(textareaElement); + linkArray.push( + App.UI.DOM.link( + "Experimental: Export this slave", + // exporter.exportActiveSlave.bind(exporter), + // If this link reloads the page, like all the other links on this line, + // then we need to set the textarea to V.activeSlave when the page loads + // (because anything it's set to by this link will be overwritten), + // and then there's no actual need for a function call at this link because the page reload will do it. + () => {}, + [], + // "Export Slave" + "Starting Girls" + ) + ); + linkArray.push( + App.UI.DOM.link( + "Experimental: Import this slave", + exporter.importActiveSlave.bind(exporter), + [], + // "Import Slave" + "Starting Girls" + ) + ); + linkArray.push( App.UI.DOM.link( "Start over with a random slave", diff --git a/src/npc/surgery/organFarm.js b/src/npc/surgery/organFarm.js index aaa889265c91e8d4f9c24f15c7a59d3e0641250c..19852bdb94e3c39a68cb8832e502d7dd00fe730b 100644 --- a/src/npc/surgery/organFarm.js +++ b/src/npc/surgery/organFarm.js @@ -197,7 +197,7 @@ App.Medicine.OrganFarm.implantAction = function(slave, organType) { /** * @param {string} organType * @param {App.Medicine.Surgery.Procedure} procedure - * @returns {DocumentFragment} + * @returns {(DocumentFragment|undefined)} */ App.Medicine.OrganFarm.implant = function(organType, procedure) { App.Medicine.OrganFarm.implantAction(procedure._slave, organType); @@ -205,7 +205,7 @@ App.Medicine.OrganFarm.implant = function(organType, procedure) { const result = App.Medicine.Surgery.apply(procedure, false); if (result === null) { Engine.play("Surgery Death"); - return; + return undefined; } const [diff, reaction] = result; diff --git a/src/player/pDildoVagina.js b/src/player/pDildoVagina.js index f7fe59425733c1ee7e36939235d6a621836484c6..7e0542309d59a53b20aa0a710f603333959693eb 100644 --- a/src/player/pDildoVagina.js +++ b/src/player/pDildoVagina.js @@ -10,7 +10,7 @@ App.UI.pDildoVagina = function() { if (S.Concubine) { r.push(`You order `, contextualIntro(V.PC, S.Concubine, true), ` to bring you a new dildo that you had made for this experience; it's fashioned after one you know from experience brings great pleasure to your tightest fucktoys. ${He} already knows something's up — after all, you don't clear your schedule for no reason — but now you can tell from ${his} ${canSee(S.Concubine) ? 'eyes' : 'face'} that ${he}'s particularly curious what you're going to do with this special dildo.`); - r.push(`A few minutes later, ${he} returns with an soft phallic object in hand.`); + r.push(`A few minutes later, ${he} returns with a soft phallic object in hand.`); } else { r.push(`You open a box that you've had sitting on your nightstand for some time. It contains a new dildo, fashioned after one you know from experience brings great pleasure to your tightest fucktoys.`); }